27 Commits
Author SHA1 Message Date
SolninjaA 9aef574167 Merge pull request #6 from SolninjaA/v1.3.1
v1.3.1 - Fix multiple context menus when the extension is refreshed on Chromium browsers
2025-01-30 19:11:39 +10:00
Solninja A 870f4aef37 Fix multiple context menus when the extension is refreshed on Chromium browsers 2025-01-30 19:10:28 +10:00
SolninjaA 6bb72d73be Merge pull request #5 from SolninjaA/v1.3.1
v1.3.1 - Fix version number
2025-01-30 19:03:02 +10:00
Solninja A 1cd26a7eb4 Fix version number 2025-01-30 19:00:20 +10:00
SolninjaA 603f15b0c1 Merge pull request #3 from SolninjaA/v1.3.1
v1.3.1
2025-01-30 18:58:47 +10:00
SolninjaA 5e2c4ca32e Merge pull request #4 from SolninjaA/all-contributors/add-SolninjaA
docs: add SolninjaA as a contributor for code, projectManagement, and doc
2025-01-30 18:49:49 +10:00
SolninjaA 3e4af6e537 Update README.md 2025-01-30 18:49:00 +10:00
SolninjaA 9aabd10fbb Update .all-contributorsrc 2025-01-30 18:47:44 +10:00
allcontributors[bot] 04a3a6ebe8 docs: update .all-contributorsrc 2025-01-30 08:45:54 +00:00
allcontributors[bot] 4e2ebac696 docs: update README.md 2025-01-30 08:45:53 +00:00
Solninja A d222666b40 Add new point to the acknowledgement section 2025-01-30 18:31:07 +10:00
Solninja A a6293facd2 Clean up code 2025-01-30 18:28:28 +10:00
Solninja A c5f8d3a7da Fix popup bugs, such as conflicting URLs and trailing whitespaces 2025-01-30 17:40:10 +10:00
Solninja A 981f6da829 Revamp popup settings 2025-01-27 17:26:25 +10:00
SolninjaA f38086593b Add Firefox Add-Ons link to README.md 2025-01-27 15:49:01 +10:00
SolninjaA fdb3f44f3b Create CODE_OF_CONDUCT.md 2025-01-27 15:37:13 +10:00
SolninjaA 74c0645136 Merge pull request #2 from SolninjaA/all-contributors/add-DarioDarko
docs: add DarioDarko as a contributor for code
2025-01-27 15:29:44 +10:00
allcontributors[bot] 9f88ba7a45 docs: update .all-contributorsrc 2025-01-27 05:28:38 +00:00
allcontributors[bot] 1aa4aa75fe docs: update README.md 2025-01-27 05:28:37 +00:00
SolninjaA d0c3386b58 Update .all-contributorsrc 2025-01-27 15:27:59 +10:00
Solninja A 538b799bab Add all contributors RC 2025-01-27 15:22:18 +10:00
SolninjaA fc8dac1c9f Add all contributors bot in README.md 2025-01-27 15:15:42 +10:00
SolninjaA 6c128226f3 v1.2.0 - Fix spacing in README.md 2025-01-27 14:47:13 +10:00
SolninjaA 458f14ba41 v1.2.0 Update README.md 2025-01-27 14:46:37 +10:00
Solninja A 7e95f1299e v1.2.0 - Update README.md 2025-01-27 14:45:10 +10:00
Solninja A 7cc4710e45 v1.2.0 - Change innerHTML to innerText 2025-01-27 14:37:25 +10:00
Solninja A ebe07d8f8b v1.2.0 - Add manual URL generation 2025-01-27 14:25:00 +10:00
10 changed files with 889 additions and 51 deletions

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
{
"projectName": "Chhoto-URL-Extension",
"projectOwner": "SolninjaA",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"contributors": [
{
"login": "SolninjaA",
"name": "SolninjaA",
"avatar_url": "https://avatars.githubusercontent.com/u/51935570?v=4",
"profile": "https://github.com/SolninjaA",
"contributions": [
"code",
"projectManagement",
"doc"
]
},
{
"login": "DarioDarko",
"name": "DarioDarko",
"avatar_url": "https://avatars.githubusercontent.com/u/154679092?v=4",
"profile": "https://github.com/DarioDarko",
"contributions": [
"code"
]
}
],
"commitType": "docs",
"commitConvention": "angular",
"contributorsPerLine": 7
}
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
code-of-conduct-report@happy-mochi.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+42 -16
View File
@@ -39,14 +39,14 @@ environment:
``` ```
## Installation ## Installation
*This extension will be published to extension stores in the near future* <a href="https://addons.mozilla.org/en-US/firefox/addon/chhoto-url/" target="_blank"><img alt="Firefox icon" src="https://imgur.com/ihXsdDO.png" width="64" height="64"></a>
Once you have installed the extension, either through an extension store or [from source](#installing-from-source), you must configure the extension. Once you have installed the extension, either through an extension store (see icons above) or [from source](#installing-from-source), you must configure the extension.
### Firefox ### Firefox
1. Right-click on the Chhoto URL extension icon (![Chhoto URL extension icon](icons/chhoto-url-16.png)). If the extension is not pinned, the Chhoto URL extension icon will be under this highlighted icon: 1. Right-click on the Chhoto URL extension icon (![Chhoto URL extension icon](icons/chhoto-url-16.png)). If the extension is not pinned, the Chhoto URL extension icon will be under this highlighted icon:
![Firefox generic extension icon](https://git.solomon.tech/solomon/Chhoto-URL-Extension/raw/branch/main/generic-extension-icon-firefox.png) ![Firefox generic extension icon](https://raw.githubusercontent.com/SolninjaA/Chhoto-URL-Extension/888138e93ba31bd4eabcb9ea3f261d4381520e9b/generic-extension-icon-firefox.png)
2. Select "Manage Extension" 2. Select "Manage Extension"
3. Select "Preferences" 3. Select "Preferences"
@@ -55,7 +55,7 @@ Once you have installed the extension, either through an extension store or [fro
### Chromium-based browsers ### Chromium-based browsers
1. **(if the extension is not pinned)** Click on the icon that is most similar to the highlighted icon in this image: 1. **(if the extension is not pinned)** Click on the icon that is most similar to the highlighted icon in this image:
![Chromium generic extension icon](https://git.solomon.tech/solomon/Chhoto-URL-Extension/raw/branch/main/generic-extension-icon-chromium.png) ![Chromium generic extension icon](https://raw.githubusercontent.com/SolninjaA/Chhoto-URL-Extension/888138e93ba31bd4eabcb9ea3f261d4381520e9b/generic-extension-icon-chromium.png)
2. **(if the extension is not pinned)** Click on the pin icon next to the Chhoto URL extension icon 2. **(if the extension is not pinned)** Click on the pin icon next to the Chhoto URL extension icon
3. Right-click on the Chhoto URL extension icon (![Chhoto URL extension icon](icons/chhoto-url-16.png)) 3. Right-click on the Chhoto URL extension icon (![Chhoto URL extension icon](icons/chhoto-url-16.png))
@@ -65,20 +65,25 @@ Once you have installed the extension, either through an extension store or [fro
## Usage ## Usage
When the Chhoto URL extension icon is clicked, the current page's URL will be shortened. The shortened link will be copied to the clipboard. When the Chhoto URL extension icon is clicked, the current page's URL will be shortened. The shortened link will be copied to the clipboard.
Alternatively, from v1.2.0 and onwards, you may right-click anywhere on a website or on the extension's icon (![Chhoto URL extension icon](icons/chhoto-url-16.png)), and click "Manually generate a Chhoto URL". This button will open a popup, allowing users to manually generate a shortened URL.
![Context menu icon](https://raw.githubusercontent.com/SolninjaA/Chhoto-URL-Extension/refs/heads/main/context-menu-button.png)
## Privacy and Security ## Privacy and Security
This extension only communicates to the Chhoto URL server instance you configure. All of the extension's data is stored locally in your local browser storage. The API key is kept in plaintext. This extension only communicates to the Chhoto URL server instance you configure. All of the extension's data is stored locally in your local browser storage. The API key is kept in plaintext.
### Extension Permissions ### Extension Permissions
| Permission | Required so that the extension can... | | Permission | Required so that the extension can... |
| --------------------------------- | ---------------------------------------------------------------------| |-----------------------------------|---------------------------------------------------------------------------------|
| [`activeTab`][tabs-api] | Get the active tab's URL | | [`activeTab`][tabs-api] | Get the active tab's URL |
| [`notifications`][notif-api] | Inform users if generating the shortened link was successful or not. | | [`notifications`][notif-api] | Inform users if generating the shortened link was successful or not. |
| [`clipboardWrite`][clipboard-api] | Copy the shortened link to your clipboard. | | [`clipboardWrite`][clipboard-api] | Copy the shortened link to your clipboard. |
| [`storage`][storage-api] | Save the extension settings in your local browser storage. | | [`storage`][storage-api] | Save the extension settings in your local browser storage. |
| [`https://*/*`][host-permission] | Contact the configured Chhoto URL server instance. | | [`https://*/*`][host-permission] | Contact the configured Chhoto URL server instance. |
| [`contextMenus`][context-menus] | Add an item (which opens a popup for manual URL generation) to the context menu |
## Installing from source ## Installing from source
1. Run `git clone https://git.solomon.tech/solomon/Chhoto-URL-Extension.git` 1. Run `git clone https://github.com/SolninjaA/Chhoto-URL-Extension.git`
2. Install the extension (see below for how to install the extension on common browsers) 2. Install the extension (see below for how to install the extension on common browsers)
### Installing from source on Firefox ### Installing from source on Firefox
@@ -95,13 +100,33 @@ This extension only communicates to the Chhoto URL server instance you configure
4. Navigate to the cloned repository folder 4. Navigate to the cloned repository folder
5. Click "Select" 5. Click "Select"
## Contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/SolninjaA"><img src="https://avatars.githubusercontent.com/u/51935570?v=4?s=100" width="100px;" alt="SolninjaA"/><br /><sub><b>SolninjaA</b></sub></a><br /><a href="https://github.com/SolninjaA/Chhoto-URL-Extension/commits?author=SolninjaA" title="Code">💻</a> <a href="#projectManagement-SolninjaA" title="Project Management">📆</a> <a href="https://github.com/SolninjaA/Chhoto-URL-Extension/commits?author=SolninjaA" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DarioDarko"><img src="https://avatars.githubusercontent.com/u/154679092?v=4?s=100" width="100px;" alt="DarioDarko"/><br /><sub><b>DarioDarko</b></sub></a><br /><a href="https://github.com/SolninjaA/Chhoto-URL-Extension/commits?author=DarioDarko" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Acknowledgements ## Acknowledgements
This project was inspired by and modified from Edward Shen's [Shlink extension][shlink-extension]. Modifications include: 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. 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. 2. Adding a popup page for manual URL generation.
3. Removing options which were either not possible to implement (because of the Chhoto URL server's limitations), or were not practical. 3. Changing the appearance of the `options.html` page.
4. Optimizing code where possible. 4. Removing options which were either not possible to implement (because of the Chhoto URL server's limitations), or were not practical.
5. Et cetera 5. Optimizing code where possible.
6. Et cetera.
## Information ## Information
| Syncing from (main repository) | Syncing to | Syncing every | | Syncing from (main repository) | Syncing to | Syncing every |
@@ -115,4 +140,5 @@ This project was inspired by and modified from Edward Shen's [Shlink extension][
[clipboard-api]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/clipboard [clipboard-api]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/clipboard
[storage-api]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage [storage-api]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage
[host-permission]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#host_permissions [host-permission]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#host_permissions
[context-menus]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus
[shlink-extension]: https://github.com/edward-shen/shlink [shlink-extension]: https://github.com/edward-shen/shlink
+94 -26
View File
@@ -19,7 +19,7 @@
/** /**
* File Overview * File Overview
* @file This file actually creates a shortened link. "options.js" is only for the options page - this file handles the main extension functionality * @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 * @copyright Solomon Tech 2025
*/ */
@@ -51,7 +51,10 @@
* @type {object} * @type {object}
* @property {number} status * @property {number} status
* @property {{success: boolean, error: boolean, shorturl: string}} json * @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.
*/ */
/** /**
@@ -74,9 +77,10 @@
* *
* @param {!URL} url * @param {!URL} url
* @param {!string} title * @param {!string} title
* @returns {!Promise<[URL, string], Error>} * @param {!string} type
* @returns {!Promise<[URL, string, string], Error>}
*/ */
function validateURL(url, title) { function validateURL(url, title, type) {
return browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => { return browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => {
// Initialize a list of protocols that are allowed if unset. // Initialize a list of protocols that are allowed if unset.
// This needs to be synced with the initialization code in options.js. // This needs to be synced with the initialization code in options.js.
@@ -97,21 +101,30 @@ function validateURL(url, title) {
} }
// Return URL and title // Return URL and title
return Promise.resolve([url, title]); return Promise.resolve([url, title, type]);
}); });
} }
/** /**
* Parses the URL outputted in the previous function, and gets the full link (i.e. URI included). * Parses the URL outputted in the previous function, and gets the full link (i.e. URI included).
* *
* @param {[!URL, string]} url - Holds long URL * @param {[!URL, string, string]} url - Holds long URL
* title - Title of the website * 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>} * @returns {!Promise<ChhotoRequest, Error>}
* If all the data was obtained, return ChhotoRequest. * If all the data was obtained, return ChhotoRequest.
* Else, return an error * Else, return an error
*/ */
function generateChhotoRequest([url, title]) { function generateChhotoRequest([url, title, type]) {
return browser.storage.local.get().then((data) => { 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 the user didn't specify an API key
if (!data.chhotoKey) { if (!data.chhotoKey) {
return Promise.reject(new Error( return Promise.reject(new Error(
@@ -119,17 +132,22 @@ function generateChhotoRequest([url, title]) {
)); ));
} }
// If the user didn't specify an API key or a host
if (!data.chhotoKey || !data.chhotoHost) {
return Promise.reject(new Error("Please configure the Chhoto URL extension. See https://git.solomon.tech/solomon/Chhoto-URL-Extension#installation for more information."));
}
// Set URL and title // Set URL and title
data.longUrl = url.href; data.longUrl = url.href;
// Make the title an empty string by default // If the request type is popup
// This will create a randomly generated string if sent to the Chhoto URL server if (type === "popup") {
data.title = ""; 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 "generateWithTitle" is true
if (data.generateWithTitle) { if (data.generateWithTitle) {
@@ -137,13 +155,15 @@ function generateChhotoRequest([url, title]) {
let wordLimit = data.titleWordLimit; let wordLimit = data.titleWordLimit;
// Format title name // 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 occurences of ' ' to '-' // Replace all occurences of ' ' to '-'
// Replace all characters except for 'a-z', '0-9', '-' and '_' with '' // 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 wordLimit is not 0, and thus limited
if (wordLimit !== "0") { // 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 // Limit the length of the short URL to the configured number
titleName = titleName.split('-', wordLimit).join('-'); titleName = titleName.split('-', wordLimit).join('-');
} }
@@ -182,10 +202,10 @@ function requestChhoto(chhotoRequest) {
longlink: chhotoRequest.longUrl, longlink: chhotoRequest.longUrl,
}), }),
}, },
// Add the HTTP status code, and requestedLink (see ChhotoResponse in type definitions for details) to the response // 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, requestedLink: `${chhotoRequest.chhotoHost}/${chhotoRequest.title}` }))) )).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 // If there was a HTTP error
// This does not activate if the Chhoto server returns a JSON response with an error // This does not activate if the Chhoto server returns a JSON response with an error
.catch(err => { .catch(err => {
// Change the error message, if there was a NetworkError // Change the error message, if there was a NetworkError
if (err.message === "NetworkError when attempting to fetch resource.") { if (err.message === "NetworkError when attempting to fetch resource.") {
@@ -212,11 +232,26 @@ function validateChhotoResponse(httpResp) {
if (httpResp.json.success) { if (httpResp.json.success) {
return httpResp.json; return httpResp.json;
} else { } else {
// If there was a URL conflict
if (httpResp.status === 409) { 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 = { const json = {
success: true, success: true,
error: false, error: false,
shorturl: httpResp.requestedLink, shorturl: `${httpResp.host}/${httpResp.requestedLink}`,
} }
return json; return json;
} }
@@ -230,11 +265,14 @@ function validateChhotoResponse(httpResp) {
/** /**
* Copies the returned shortened link to the clipboard. * 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, Error>} `ChhotoJSON`, unmodified, on * @returns {!Promise<ChhotoJSON, Error>} `ChhotoJSON`, unmodified, on
* success, or an error indicating that we failed to copy to the clipboard. * success, or an error indicating that we failed to copy to the clipboard.
*/ */
function copyLinkToClipboard(chhotoResp) { function copyLinkToClipboard(chhotoResp) {
// Send finished message
browser.runtime.sendMessage({message: "finished"});
// Chrome requires this hacky workaround :( // Chrome requires this hacky workaround :(
if (typeof chrome !== "undefined") { if (typeof chrome !== "undefined") {
const prevSelected = document.activeElement; const prevSelected = document.activeElement;
@@ -289,20 +327,50 @@ function notifyError(error) {
}); });
} }
/** /**
* Main function for generating a shortened link. * Main function for generating a shortened link.
*/ */
function generateChhoto() { function generateChhoto() {
// Define the type of the request
const type = "background";
// Call functions
browser.tabs browser.tabs
.query({ active: true, currentWindow: true }) .query({ active: true, currentWindow: true })
.then(tabData => validateURL(new URL(tabData[0].url), tabData[0].title)) .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(generateChhotoRequest)
.then(requestChhoto) .then(requestChhoto)
.then(validateChhotoResponse) .then(validateChhotoResponse)
.then(copyLinkToClipboard) .then(copyLinkToClipboard)
.then(notifySuccess) .then(notifySuccess)
.catch(notifyError); .catch(notifyError);
} };
// When the extension icon is clicked, call the function above // When the extension icon is clicked, call the function above
browser.browserAction.onClicked.addListener(generateChhoto); browser.browserAction.onClicked.addListener(generateChhoto);
// Create a context menu
browser.contextMenus.removeAll();
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"});
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+5 -4
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 2, "manifest_version": 2,
"name": "Chhoto URL", "name": "Chhoto URL",
"version": "1.1.0", "version": "1.3.1",
"description": "An unofficial extension for shortening URLs using the Chhoto URL API. Requires a Chhoto URL instance.", "description": "An unofficial extension for shortening URLs using the Chhoto URL API. Requires a Chhoto URL instance.",
"icons": { "icons": {
"16": "icons/chhoto-url-16.png", "16": "icons/chhoto-url-16.png",
@@ -16,10 +16,11 @@
"notifications", "notifications",
"clipboardWrite", "clipboardWrite",
"storage", "storage",
"https://*/*" "https://*/*",
"contextMenus"
], ],
"options_ui": { "options_ui": {
"page": "options.html" "page": "options/options.html"
}, },
"browser_action": { "browser_action": {
"default_icon": { "default_icon": {
@@ -35,7 +36,7 @@
"background": { "background": {
"scripts": [ "scripts": [
"lib/browser-polyfill.min.js", "lib/browser-polyfill.min.js",
"background.js" "background/background.js"
] ]
} }
} }
+14 -4
View File
@@ -91,7 +91,7 @@
<body> <body>
<div class="header"> <div class="header">
<img src="icons/chhoto-url-48.png"></img> <img src="/icons/chhoto-url-48.png"></img>
<h1>Chhoto URL Settings</h1> <h1>Chhoto URL Settings</h1>
</div> </div>
@@ -137,7 +137,7 @@
<p class="note">Disabling all filters will allow a link to be created from any URL.</p> <p class="note">Disabling all filters will allow a link to be created from any URL.</p>
<div class="settings checkbox"> <div class="settings checkbox">
<h2>URL generation</h2> <h2>URL Generation</h2>
<label> <label>
Create shortened links with the website's title Create shortened links with the website's title
<input type="checkbox" id="generate-with-title"> <input type="checkbox" id="generate-with-title">
@@ -150,8 +150,18 @@
<p id="message2" class="note">Inputted value is not a number.</p> <p id="message2" class="note">Inputted value is not a number.</p>
</div> </div>
<script type="application/javascript" src="lib/browser-polyfill.min.js"></script> <div class="settings checkbox">
<script type="application/javascript" src="options.js"></script> <h2>Popup Settings</h2>
<label>
Automatically insert long URL into the popup
<input type="checkbox" id="auto-insert-popup">
<p class="note">This will only autofill the long URL if the page's protocol is supported (see the allowed protocols setting).</p>
</label>
</div>
<script type="application/javascript" src="/lib/browser-polyfill.min.js"></script>
<script type="application/javascript" src="/options/options.js"></script>
</body> </body>
</html> </html>
+25 -1
View File
@@ -40,6 +40,7 @@ const generateWithTitleEle = document.getElementById("generate-with-title");
const titleWordLimitLabelEle = document.getElementById("title-word-limit-label"); const titleWordLimitLabelEle = document.getElementById("title-word-limit-label");
const titleWordLimitEle = document.getElementById("title-word-limit"); const titleWordLimitEle = document.getElementById("title-word-limit");
const message2Ele = document.getElementById("message2"); const message2Ele = document.getElementById("message2");
const autoInsertPopupEle = document.getElementById("auto-insert-popup");
// Get the browser storage // Get the browser storage
const browserStorage = browser.storage.local; const browserStorage = browser.storage.local;
@@ -164,7 +165,21 @@ titleWordLimitEle.oninput = (event) => {
}; };
} }
function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols, generateWithTitle, titleWordLimit }) { // Automatically insert long URL into popup
autoInsertPopupEle.onclick = () => {
// Get browser storage
browserStorage.get("autoInsertPopup").then(({ autoInsertPopup }) => {
// Get value
autoInsertPopup = autoInsertPopupEle.checked;
// Save value
browserStorage.set({ autoInsertPopup });
});
};
function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols, generateWithTitle, titleWordLimit, autoInsertPopup }) {
hostKeyEle.value = chhotoHost || ""; hostKeyEle.value = chhotoHost || "";
apiKeyEle.value = chhotoKey || ""; apiKeyEle.value = chhotoKey || "";
@@ -191,16 +206,25 @@ function setCurrentChoice({ chhotoHost, chhotoKey, allowedProtocols, generateWit
browserStorage.set({ titleWordLimit: titleWordLimit }); browserStorage.set({ titleWordLimit: titleWordLimit });
} }
// If autoInsertPopup is undefined, set the default value
if (autoInsertPopup === undefined) {
autoInsertPopup = false;
browserStorage.set({ autoInsertPopup: autoInsertPopup });
}
// Initialize a list of protocols that are allowed if unset. This needs // Initialize a list of protocols that are allowed if unset. This needs
// to be synced with the initialization code in background.js#validateURL. // to be synced with the initialization code in background.js#validateURL.
allowedProtocols = new Set(allowedProtocols); allowedProtocols = new Set(allowedProtocols);
// Update the checkboxes to display the correct value
AllowHttpEle.checked = allowedProtocols.has("http:"); AllowHttpEle.checked = allowedProtocols.has("http:");
AllowHttpsEle.checked = allowedProtocols.has("https:"); AllowHttpsEle.checked = allowedProtocols.has("https:");
AllowFileEle.checked = allowedProtocols.has("file:"); AllowFileEle.checked = allowedProtocols.has("file:");
AllowFtpEle.checked = allowedProtocols.has("ftp:"); AllowFtpEle.checked = allowedProtocols.has("ftp:");
generateWithTitleEle.checked = generateWithTitle; generateWithTitleEle.checked = generateWithTitle;
autoInsertPopupEle.checked = autoInsertPopup;
// Set default display // Set default display
let display = "none"; let display = "none";
+146
View File
@@ -0,0 +1,146 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
display: flex;
flex-direction: column;
padding: 5px;
height: 300px;
font-size: 20px;
font-family: sans-serif;
margin-bottom: 20px;
}
.header {
display: flex;
align-items: center;
}
.header img {
padding-right: 10px;
}
#close {
position: absolute;
top: 20px;
right: 20px;
border: black 1px solid;
border-radius: 7px;
background-color: white;
min-height: 30px;
min-width: 75px;
cursor: pointer;
}
.generate-button,
#url-conflict-yes,
#url-conflict-no {
border: black 1px solid;
border-radius: 7px;
background-color: white;
min-height: 50px;
min-width: 200px;
cursor: pointer;
}
#close:hover,
.generate-button:hover,
#url-conflict-yes:hover,
#url-conflict-no:hover {
background-color: #bbb;
}
.generate {
display: flex;
align-items: baseline;
}
label {
padding: 0 10px;
align-items: baseline;
padding: 5px 0;
}
input {
min-width: 250px;
min-height: 17px;
padding: 5px;
border: 2px solid black;
border-radius: 7px;
text-align: center;
font-size: 15px;
}
#message,
#message2,
#message3,
#url-conflict,
#delete-error {
display: none;
}
.warning {
color: red;
display: block !important;
}
.shown {
display: block !important;
}
</style>
</head>
<body>
<div class="header">
<img src="/icons/chhoto-url-48.png"></img>
<h1>Generate a Chhoto URL</h1>
</div>
<button id="close" type="button">&#10006; Close</button>
<form id="generate">
<div class="generate">
<label>
Short URL
<input autofocus required type="text" id="shorturl" placeholder="interesting-short-url">
<p id="message"></p>
</label>
</div>
<div class="generate">
<label>
Long URL
<input required type="text" id="longurl" placeholder="https://example.com">
<p id="message2">The provided URL is invalid.</p>
</label>
</div>
<div class="generate">
<label>
<input class="generate-button" type="submit" value="Shorten!">
<p id="message3">Error: Required fields are missing.</p>
</label>
</div>
</form>
<div id="url-conflict">
<h2 id="url-conflict-header"></h2>
<h3>Would you like to replace it?</h3>
<input id="url-conflict-yes" type="submit" value="Yes">
<input id="url-conflict-no" type="submit" value="No (append a random string)">
<p>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.</p>
</div>
<div id="delete-error">
<h2>Error: Failed to replace link</h2>
<p id="delete-error-message"></p>
</div>
<script type="application/javascript" src="/lib/browser-polyfill.min.js"></script>
<script type="application/javascript" src="/popup/popup.js"></script>
</body>
</html>
+401
View File
@@ -0,0 +1,401 @@
/*
* 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 handles the logic behind the popup page. This script will call a "background.js" function to generate a shortened URL.
* @copyright Solomon Tech 2025
*/
// Use JavaScript's "strict" mode
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode
"use strict";
// Get elements
const closeEle = document.querySelector("#close");
const shortURLEle = document.querySelector("#shorturl");
const longURLEle = document.querySelector("#longurl");
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
*/
// If a URL was passed in the request
if (requestValue) {
browser.storage.local.get().then(( data ) => {
if (data.autoInsertPopup !== undefined && data.autoInsertPopup) {
let allowedProtocols;
// Initialize a list of protocols that are allowed if unset.
if (data.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 = data.allowedProtocols;
allowedProtocols = new Set(allowedProtocols);
}
// Try and catch structure
try {
// Define the URL
const url = new URL(requestValue);
// Ensure the URL has a valid protocol
if (allowedProtocols.size > 0 && !allowedProtocols.has(url.protocol)) {
throw new Error("The URL is invalid");
};
////// If anything beyond this point is trigerred, the URL protocol is valid. //////
// Reassign the long url
longURLEle.value = url;
longurl = url;
} catch (error) {
console.log(`Error while auto inserting the long URL - ${error}`);
};
};
});
};
/**
* When the short URL is inputted
* */
// When the short URL is inputted
shortURLEle.oninput = (event) => {
if (event.type === "click") {
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
browser.storage.local.get().then((data) => {
// Set the word limit
const wordLimit = data.titleWordLimit;
// Get the configured host
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.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
if (chhotoHost !== undefined) {
// Inform the user
messageEle.innerText = `The short URL will be generated as: ${chhotoHost}/${shortURLText}.`;
} else {
// If the configured host is undefined
messageEle.innerText = "The Chhoto URL host has not been configured. Cannot generate shortened URL.";
}
// Set the short URL
shorturl = shortURLText;
// Show the message
messageEle.classList.add("shown");
});
} 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") {
event.preventDefault();
};
// If the long URL value is greater than 0 (i.e. not an empty string)
if (longURLEle.value.length > 0) {
// Get the allowed protocols
browser.storage.local.get("allowedProtocols").then(({ allowedProtocols }) => {
// Initialize a list of protocols that are allowed if unset.
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);
}
// Try and catch structure
try {
// Define the URL
const url = new URL(longURLEle.value);
// Ensure the URL has a valid protocol
if (allowedProtocols.size > 0 && !allowedProtocols.has(url.protocol)) {
throw new Error("The URL is invalid");
};
////// If anything beyond this point is trigerred, the URL protocol is valid. //////
// Reassign the long url
longurl = url;
// Remove any warning styles which may be active
message2Ele.classList.remove("warning");
longURLEle.style.color = "black";
} catch {
// If the URL is invalid, add warning styles
message2Ele.classList.add("warning");
longURLEle.style.color = "red";
};
});
} else {
// If the long URL is an empty string, hide the warning styling
message2Ele.classList.remove("warning");
longURLEle.style.color = "black";
}
};