Skip to content

Commit

Permalink
Merge branch 'dev' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
The-breakbar committed Feb 13, 2024
2 parents cb33d66 + f392061 commit ea6397a
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 184 deletions.
80 changes: 36 additions & 44 deletions DEVELOPMENT.md

Large diffs are not rendered by default.

39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
# TISS Lightning Registrator

![Screenshot of the extension being shown in three different browsers](images/Screenshots.png)
[![Screenshot of the extension being shown in three different browsers](images/Screenshots.png)](https://chromewebstore.google.com/detail/aafcdagpbbpnjpnfofompbhefgpddimi)

Introducing TISS Lightning Registrator, a handy browser extension that takes the hassle out of TISS registrations by automatically signing you up for LVAs, groups, or exams as soon as they open. Start the extension before a registration begins, and once it does, experience lightning-fast registration that is often completed in less than a second. Additionally this streamlined process works quietly in the background, allowing you to multitask and browse other tabs, while the extension handles the registration process efficiently.
TISS Lightning Registrator is a browser extension that takes the hassle out of TISS registrations. Automatically sign up for courses, groups, or exams as soon as they open. Start the extension before a registration begins, and experience lightning-fast registration that is often completed in less than a second.

## Installing
## Download

The extension will be available on extension stores soon, until then it has to be installed manually. The extension is available for Chrome and other Chromium browsers (Opera, Edge, etc) as well as Firefox.
### Chrome / Opera / Edge

[**Download from the Chrome Web Store**](https://chromewebstore.google.com/detail/aafcdagpbbpnjpnfofompbhefgpddimi)

### Firefox

Navigate to the GitHub [releases](https://github.com/The-breakbar/TISS-Lightning-Registrator/releases) and download the latest `TISS-Lightning-Registrator-<version>-Firefox.xpi` file (note the `.xpi` file extension). The browser will prompt you to install the extension, if not, drag the downloaded file into the browser.

### Manual installation

<details>
<summary>Show manual installation instructions</summary>

**This installation method is not recommended for regular users, as you will not receive updates this way.** This method should only be used for development purposes or if you are unable to use the other installation methods. New updates will have to be manually downloaded and installed.

### Chrome / Opera / Edge

1. Download the zip of the latest release from the GitHub [releases](https://github.com/The-breakbar/TISS-Lightning-Registrator/releases) and unpack it.
1. Download the zip of the latest unpacked release from the GitHub [releases](https://github.com/The-breakbar/TISS-Lightning-Registrator/releases) and extract it (or clone the repo).
2. Navigate to the browser page of your installed extensions. It can be found under "Manage extensions" in your browser options or by going to the following links depending on your browser:

- Chrome: `chrome://extensions`
Expand All @@ -19,20 +32,16 @@ The extension will be available on extension stores soon, until then it has to b

3. Enable "Developer mode" in the top right (left side for Edge).
4. Click the "Load unpacked extension" button and select the unpacked folder which you downloaded (make sure you select the folder which contains all the files).
5. Done! Pin the extension to your top bar and use it by clicking on its icon.

### Firefox

Due to the manual installation process, Firefox unfortunately requires the extension to be reinstalled every time the browser is restarted. The extension will be available on the Firefox Add-ons store soon.
Due to the manual installation process, Firefox unfortunately requires the extension to be reinstalled every time the browser is restarted.

1. Download the zip of the latest Firefox release from the GitHub [releases](https://github.com/The-breakbar/TISS-Lightning-Registrator/releases) and unpack it.
1. Download the zip of the latest unpacked Firefox release from the GitHub [releases](https://github.com/The-breakbar/TISS-Lightning-Registrator/releases) and unpack it (or clone the repo).
2. Navigate to `about:debugging#/runtime/this-firefox`.
3. Click the "Load Temporary Add-on..." button and select the `manifest.json` file in the unpacked folder which you downloaded.
4. Done! Pin the extension to your top bar and use it by clicking on its icon.

### Updating

To update the extension, simply remove the existing version and install the newest version with the same steps.
</details>

## How it works

Expand All @@ -44,4 +53,8 @@ As the extension is based very tightly on the TISS website, any future changes t

## Bug reports

All bug reports are appreciated! If you find any bugs, feel free to open an [issue](https://github.com/The-breakbar/TISS-Lightning-Registrator/issues). Error messages can be found in the console of the tab where the extension was used (they're only present as long as you don't refresh or change the page). There might also be an "Errors" button on the browser settings page where the extension was installed. It contains errors from the pop-up window, as those are not logged to the regular console.
All bug reports are appreciated! If you find any problems, feel free to open an [issue](https://github.com/The-breakbar/TISS-Lightning-Registrator/issues).

## License

This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
20 changes: 15 additions & 5 deletions background.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
const client = typeof browser === "undefined" ? chrome : browser;

// Set access level to allow content scripts to access session storage
chrome.storage.session.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" });
//client.storage.local.setAccessLevel({ accessLevel: "TRUSTED_AND_UNTRUSTED_CONTEXTS" });

// Remove tasks if their tab is closed or updated
// Success and failed tasks don't need to be removed, as they are already finished and will expire
let removeTask = async (tabId) => {
let task = (await chrome.storage.session.get(tabId.toString()))[tabId];
let task = (await client.storage.local.get(tabId.toString()))[tabId];
if (task?.status == "success" || task?.status == "failure") return;
chrome.storage.session.remove(tabId.toString());
client.storage.local.remove(tabId.toString());
};
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
client.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status == "loading") removeTask(tabId);
});
chrome.tabs.onRemoved.addListener(async (tabId, removeInfo) => removeTask(tabId));
client.tabs.onRemoved.addListener(async (tabId, removeInfo) => removeTask(tabId));

// Remove all tasks when the browser starts (to ignore tasks from previous sessions)
client.runtime.onStartup.addListener(async () => {
let tasks = await client.storage.local.get(null);
for (let task in tasks) {
client.storage.local.remove(task);
}
});
4 changes: 3 additions & 1 deletion content-scripts/getPageInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
// option.slot.end : End time of slot
// option.slot.participants : Registered/available participants

const client = typeof browser === "undefined" ? chrome : browser;

// Determine page type (this variable is accessible from all other content scripts)
let pageType;
if (/courseRegistration/.test(window.location.href)) pageType = "lva";
Expand All @@ -50,7 +52,7 @@ if (expandElement) {
}

// Create object to store page info and bind main callback to message listener
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
client.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Check if the message is a request to get page info
if (message.action != "getPageInfo") return;

Expand Down
4 changes: 2 additions & 2 deletions content-scripts/infoMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const STATUS_TEXT = {
failure: "Failure"
};

chrome.storage.onChanged.addListener((changes, area) => {
if (area != "session") return;
client.storage.onChanged.addListener((changes, area) => {
if (area != "local") return;

// If the update is for this tab, update the message (tabId is from sendRegistration.js)
if (!changes[tabId]?.newValue) return;
Expand Down
8 changes: 4 additions & 4 deletions content-scripts/resultHandler.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
// This is a content script responsible for handling the results of the registration attempts
const TASK_EXPIRY = 60000;

// Helper function to update the task in the session storage
// Helper function to update the task in local storage
let updateTask = async (tabId, update) => {
let task;
// Just in case the task is not stored yet
// During regular operation it is always stored, but the execution might reach this point before it is added to session storage
// During regular operation it is always stored, but the execution might reach this point before it is added to local storage
while (!task) {
task = (await chrome.storage.session.get(tabId.toString()))[tabId];
task = (await client.storage.local.get(tabId.toString()))[tabId];
}
if (task.status == "queued" || task.status == "running") {
task = Object.assign(task, update);
chrome.storage.session.set({ [tabId]: task });
client.storage.local.set({ [tabId]: task });
}
};

Expand Down
89 changes: 37 additions & 52 deletions content-scripts/sendRegistration.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ let logExactTime = (...message) => {
let tabId;

// This is the main callback that is run when the extension initiates the registration
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
client.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action != "sendRegistration") return;
logExactTime("Received registration request...");

Expand Down Expand Up @@ -81,25 +81,33 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
sendResponse();
});

// Helper function for getting the register button
let getButtonFromPage = (page, optionId) => {
let options = page.querySelectorAll("#contentInner .groupWrapper");
let button;
if (pageType == "lva") button = options[0].querySelector(`input[id^="registrationForm"]`);
else
button = Array.from(options)
.map((option) => option.querySelector(`input[id*="${optionId}"]`))
.find((button) => button);
return button;
};

// Continously refresh the page until the register button is found, then get the ViewState and start the registration loop
// Requests are sent in series at, if the button is not found after the specified time, the loop will stop
let refreshLoop = async (optionId, slot) => {
let stopTime = Date.now() + START_OFFSET + STOP_OFFSET;
let viewState;
let viewState, buttonId;

while (!viewState && Date.now() < stopTime) {
logExactTime("Refreshing page...");
// Refresh the page and check if the button is on the page
let pageDocument = await getPage();
let options = pageDocument.querySelectorAll("#contentInner .groupWrapper");

let button;
if (pageType == "lva") button = options[0].querySelector("#registrationForm\\:j_id_6z");
else button = Array.from(options).find((option) => option.querySelector(`input[id*="${optionId}"]`));
let button = getButtonFromPage(pageDocument, optionId);

// If the button was found, extract the ViewState
if (button) {
viewState = pageDocument.querySelector(`input[name="javax.faces.ViewState"]`).value;
buttonId = button.id;
}
}

Expand All @@ -114,19 +122,19 @@ let refreshLoop = async (optionId, slot) => {
}

// Start the registration loop
registerLoop(viewState, optionId, slot);
registerLoop(viewState, buttonId, optionId, slot);
};

// Updates the status of the registration task
// This is a possible race condition for a visual bug, however the requests would have to be faster than the updating (which is highly unlikely)
let updateTaskToRunning = async () => {
let task;
while (!task) {
task = (await chrome.storage.session.get(tabId.toString()))[tabId];
task = (await client.storage.local.get(tabId.toString()))[tabId];
}
if (task.status == "queued") {
task.status = "running";
chrome.storage.session.set({ [tabId]: task });
client.storage.local.set({ [tabId]: task });
}
};

Expand All @@ -138,7 +146,7 @@ let updateTaskToRunning = async () => {
// - While sending many requests at once, it has been observed that the response is an error, even if the registration was successfully processed internally
// All the following requests will then get a response which says they're already registered, which causes issues for the result handler
// The observed issues are not fully understood, so it could be considered to change this to a parallel request loop in the future, if it's faster
let registerLoop = async (firstViewState, optionId, slot) => {
let registerLoop = async (firstViewState, buttonId, optionId, slot) => {
logExactTime("Valid ViewState obtained, starting register loop...");

// Update the status of the registration task
Expand All @@ -160,12 +168,7 @@ let registerLoop = async (firstViewState, optionId, slot) => {
// Refresh the page and get the ViewState
logExactTime("Refreshing for new ViewState...");
let pageDocument = await getPage();

// Check if we're registered already
let options = pageDocument.querySelectorAll("#contentInner .groupWrapper");
let button;
if (pageType == "lva") button = options[0].querySelector("#registrationForm\\:j_id_6x");
else button = Array.from(options).find((option) => option.querySelector(`input[id*="${optionId}"]`));
let button = getButtonFromPage(pageDocument, optionId);

// If the button says we're already registered, stop the loop
// This handles the exceptionally rare case that the registration was processed internally, but the response was an error
Expand All @@ -182,7 +185,7 @@ let registerLoop = async (firstViewState, optionId, slot) => {
}

// Throws an error here if the request fails in any way
response = await sendRequest(viewState, optionId, slot);
response = await sendRequest(viewState, buttonId, slot);

// Reaching this point means the request succeeded
// (Note that this doesn't mean the registration was successful, as the response has to be checked)
Expand Down Expand Up @@ -214,31 +217,9 @@ const GROUP_ENDPOINT = "https://tiss.tuwien.ac.at/education/course/groupList.xht
const EXAM_ENDPOINT = "https://tiss.tuwien.ac.at/education/course/examDateList.xhtml";
const CONFIRM_ENDPOINT = "https://tiss.tuwien.ac.at/education/course/register.xhtml";

const CONFIRM_DATA = {
"regForm:j_id_30": "Register",
regForm_SUBMIT: "1"
};
const LVA_DATA = {
"registrationForm:j_id_6z": "Register",
registrationForm_SUBMIT: "1"
};
// The group and exam data needs the id of the option to be inserted
let getGroupData = (groupId) => {
let data = { groupContentForm_SUBMIT: "1" };
let idKey = `groupContentForm:${groupId}:j_id_a1`;
data[idKey] = "Register";
return data;
};
let getExamData = (examId) => {
let data = { examDateListForm_SUBMIT: "1" };
let idKey = `examDateListForm:${examId}:j_id_9u`;
data[idKey] = "Register";
return data;
};

// This function attempts to send the two POST requests required to register and returns a promise of the body of the second request
// If any of the requests fail, it will throw an error (caused by the validateResponse function)
let sendRequest = async (viewState, optionId, slot) => {
let sendRequest = async (viewState, buttonId, slot) => {
// Define the request body
let bodyData = {
dspwid: windowId,
Expand All @@ -247,35 +228,39 @@ let sendRequest = async (viewState, optionId, slot) => {
};

// Create the body together with the additional required data and define endpoint
let targetUrl, body;
let targetUrl, firstBody;
if (pageType == "lva") {
targetUrl = LVA_ENDPOINT;
body = { ...bodyData, ...LVA_DATA };
firstBody = { ...bodyData, registrationForm_SUBMIT: "1" };
} else if (pageType == "group") {
targetUrl = GROUP_ENDPOINT;
body = { ...bodyData, ...getGroupData(optionId) };
firstBody = { ...bodyData, groupContentForm_SUBMIT: "1" };
} else if (pageType == "exam") {
targetUrl = EXAM_ENDPOINT;
body = { ...bodyData, ...getExamData(optionId) };
firstBody = { ...bodyData, examDateListForm_SUBMIT: "1" };
}
firstBody[buttonId] = "Register";

// Define the request payload
let payload = {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(body).toString() // Encode the body as a query string
body: new URLSearchParams(firstBody).toString() // Encode the body as a query string
};

// Send the first request
logExactTime("Sending register request with body: ", body);
logExactTime("Sending register request with body: ", firstBody);
let firstResponse = await fetch(targetUrl, payload);
validateResponse(firstResponse);
let pageDocument = new DOMParser().parseFromString(await firstResponse.text(), "text/html");

// Get the new ViewState from the response and add it to the payload
bodyData["javax.faces.ViewState"] = pageDocument.querySelector(`input[name="javax.faces.ViewState"]`).value;
// Get the new ViewState and button id from the response and add it to the payload
let secondBody = { ...bodyData, regForm_SUBMIT: "1" };
secondBody["javax.faces.ViewState"] = pageDocument.querySelector(`input[name="javax.faces.ViewState"]`).value;
let confirmButton = pageDocument.querySelector(`#contentInner #regForm input`);
secondBody[confirmButton.id] = "Register";

// If the registration option has slots, get the slot id from the response and add it to the payload
if (slot) {
Expand All @@ -285,10 +270,10 @@ let sendRequest = async (viewState, optionId, slot) => {
}

// Update the body with the new data and encode it
payload.body = new URLSearchParams({ ...bodyData, ...CONFIRM_DATA }).toString();
payload.body = new URLSearchParams(secondBody).toString();

// Send the second request
logExactTime("Sending confirm request with body: ", { ...bodyData, ...CONFIRM_DATA });
logExactTime("Sending confirm request with body: ", secondBody);
let secondResponse = await fetch(CONFIRM_ENDPOINT, payload);
validateResponse(secondResponse);

Expand Down
Binary file added images/github_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let tabId;
initTaskRemovalTimeouts();

// Check if tab is a registration page
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
client.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
tabId = tabs[0].id;
let tabUrl = tabs[0].url;
if (!/https:\/\/tiss.tuwien.ac.at\/education\/course\/(courseRegistration|groupList|examDateList)/.test(tabUrl)) return;
Expand All @@ -25,7 +25,7 @@ chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
// This is necessary because the popup can be opened before the content script has loaded
while (!pageInfo) {
// Catch empty because it just means the tab hasn't loaded yet
pageInfo = await chrome.tabs.sendMessage(tabId, { action: "getPageInfo" }).catch(() => {});
pageInfo = await client.tabs.sendMessage(tabId, { action: "getPageInfo" }).catch(() => {});
}

// Hide loading text
Expand Down
Loading

0 comments on commit ea6397a

Please sign in to comment.