Skip to content

feat: Add button to publish to Greasy Fork#2458

Open
danielzgtg wants to merge 3 commits into
violentmonkey:masterfrom
danielzgtg:feat/greasyForkPublishButton
Open

feat: Add button to publish to Greasy Fork#2458
danielzgtg wants to merge 3 commits into
violentmonkey:masterfrom
danielzgtg:feat/greasyForkPublishButton

Conversation

@danielzgtg

@danielzgtg danielzgtg commented Mar 4, 2026

Copy link
Copy Markdown

See #2425

Screenshots

Before publish: Publish userscript button appears

publish userscript button appears above editor

After successful publish: Push update button appears

push update button appears above editor

3rd-party read-only: No button added

no extra button next to READ-ONLY

3rd-party modified: No button added

Anything that didn't go through my workflow is considered 3rd-party.

no extra button when we didn't go through my process

If you want to have it recognize a userscript as 1st-party:

  1. Remove @updateURL and @downloadURL
  2. Click "Publish userscript"
  3. Close the tab that appears
  4. Refresh the Greasy Fork tab that contains the green "Reinstall version" button

This way, upon reopening, the "Push update" button should appear

@tophf

tophf commented Mar 4, 2026

Copy link
Copy Markdown
Member

This is great, but I have some issues. Instead of opening a new document I'd like to show the inputs in the current tab and use fetch() to submit a FormData object. I'd also like to avoid showMessage and instead show the hint as text alongside those inputs. It would also make sense to show the Publish button alongside the Save button if the script is connected.

Could you address these?

@danielzgtg

Copy link
Copy Markdown
Author

Instead of opening a new document I'd like to show the inputs in the current tab and use fetch() to submit a FormData object

Due to security concerns, Greasy Fork refused to allow this and requires user interaction before an update can be pushed. Userstyles.world only refuses this on the first upload i.e. publish.

It would also make sense to show the Publish button alongside the Save button if the script is connected

I can move the Publish button to the top bar as requested. Not sure how squished "text alongside those inputs" will be.

avoid showMessage

That was a temporary workaround. I'll investigate how to detect when the publish is successful in Greasy Fork, and how to make the updateURL reload automatically without closing the editor.

@Zulenka

Zulenka commented Mar 4, 2026

Copy link
Copy Markdown

You can connect GreasyFork to a git repo and push the repo.
image

@tophf

tophf commented Mar 4, 2026

Copy link
Copy Markdown
Member

I don't see where GreasyFork doesn't allow fetch(), but if it's indeed so, you can embed the form element in the current page and set its target=_blank or use an iframe.

Not sure how squished "text alongside those inputs" will be.

The UI with the inputs will be shown as a part of the current editor page, it can be a panel between the top panel and the code or it can overlay the code. There'll be plenty of space, no need to squish.

detect when the publish is successful

Probably use a temporarily added webRequest.onCompleted listener for the form action's URL.

@tophf

tophf commented Mar 4, 2026

Copy link
Copy Markdown
Member

...or form's target may be the name of an invisible iframe.

@JasonBarnabe

Copy link
Copy Markdown

I don't see where GreasyFork doesn't allow fetch(), but if it's indeed so, you can embed the form element in the current page and set its target=_blank or use an iframe.

Greasy Fork has anti-CSRF measures that prohibit POST requests coming from off-site. The "prefill" URL is exempted.

@tophf

tophf commented Mar 5, 2026

Copy link
Copy Markdown
Member

Extensions can modify the "coming from" header, which I guess is Referer. If it's Origin I haven't tried it myself. In either case it's done by adding a temporary chrome.webRequest.onBeforeSendHeaders listener for MV2.

@tophf

tophf commented Mar 5, 2026

Copy link
Copy Markdown
Member

But even without patching, it might be possible to use a visible iframe embedded in the editor page.

@danielzgtg

Copy link
Copy Markdown
Author

GreasyFork to a git repo and push the repo

How is ViolentMonkey going to push a repo stored on disk? I edit inside ViolentMonkey so that I can edit-then-refresh 6 times per minute.

doesn't allow fetch

The point is that userscript writers want to upload images and select their preferred privacy radio button in the form that opens.

no need to squish.

I will attempt to remove the hint that's no longer necessary once I make the reload automatic.

webRequest.onCompleted listener

The form we submit does not contain the updateURL. We need to wait for the userscript writer to submit another form on greasyfork.org to have an updateURL. I think I'll listen for the "Reinstall" button.

visible iframe embedded in the editor page

The iframe would be smaller than desired. I prefer a new tab or at least a popup window like in Stylus to have lots of screen space to write the userscript description.

form element in the current page and set its target=_blank

I will do this. I'm a bit new to Vue.js

@tophf

tophf commented Mar 5, 2026

Copy link
Copy Markdown
Member

Ah so if it's similar to Stylus integration with userstyles.world then it might make sense to specify the window as a popup:

const {availWidth: w, availHeight: h} = screen;
window.open('', '_blank', `popup=1,left=${w / 4},top=${h / 4},width=${w / 2},height=${h / 2}`);

@JasonBarnabe

Copy link
Copy Markdown

Extensions can modify the "coming from" header, which I guess is Referer. If it's Origin I haven't tried it myself. In either case it's done by adding a temporary chrome.webRequest.onBeforeSendHeaders listener for MV2.

The anti-CSRF check is not using the referer or origin headers. It is based on a value stored in session and a hidden input on the page. The anti-CSRF check is intentional and I would not appreciate efforts to circumvent it. The intended behaviour here is to prefill the form with the new code, and for the user to press a button on greasyfork.org to actually submit it.

@danielzgtg danielzgtg force-pushed the feat/greasyForkPublishButton branch from 6f7b191 to aa96741 Compare May 29, 2026 15:19
@danielzgtg danielzgtg marked this pull request as draft May 29, 2026 15:19
@danielzgtg danielzgtg force-pushed the feat/greasyForkPublishButton branch from aa96741 to b32d166 Compare May 30, 2026 04:33
@danielzgtg danielzgtg marked this pull request as ready for review May 30, 2026 04:33
@danielzgtg

Copy link
Copy Markdown
Author

@tophf Your idea of a "form element in the current page" simplified my code. I added automatic detection of the "Reinstall version" button after going through "Publish userscript", so showMessage and hints aren't needed anymore.

I added screenshots at the top of the 4 cases I tested working. This PR is now ready for others to test.

@tophf tophf force-pushed the feat/greasyForkPublishButton branch from b32d166 to 3f4030f Compare May 30, 2026 06:54
@danielzgtg danielzgtg force-pushed the feat/greasyForkPublishButton branch from 3f4030f to ad6c438 Compare May 30, 2026 14:30
@tophf tophf linked an issue May 31, 2026 that may be closed by this pull request
@tophf

tophf commented May 31, 2026

Copy link
Copy Markdown
Member

I really want it to open inside an iframe inside the editor tab and judging by the HTTP response headers greasyfork doesn't forbid opening itself inside an iframe. Assuming it's added to the Vue template and declared as const $iframe = ref()it will redirect similarly to what you do now:

const iframe = document.createElement('iframe');
const form = document.createElement('form');
const textarea = document.createElement('textarea');
form.hidden = true;
form.method = 'post';
textarea.name = 'script_version[code]';
textarea.value = $codeComp.getRealContent();
document.body.append(iframe);
iframe.contentDocument.body.append(form);
form.submit();

Publish is sufficient as a label because we already in a userscript editing mode. You can add a title="greasyfork.org" if you want, but it's probably unnecessary as it's clear from the UI that opens.

I think the label doesn't need to change to Push update even when the script is connected to greasyfork. We can probably colorize the button in this case to something greenish or bluish.

I also have a lot of trivial suggestions for the actual code but we can deal with that later or I can do it myself after we establish the fundamental UX. For example, in injected folder the code must avoid using any built-in methods that weren't safely saved, so web/index.js should be restored and instead content/index.js should do something like this:

diff --git a/src/injected/content/index.js b/src/injected/content/index.js
@@ -7 +7 @@ import './tabs';
-import { sendCmd } from './util';
+import { getAttribute, querySelector, sendCmd } from './util';
@@ -44 +44,12 @@ async function init() {
-    addHandlers({ GetScriptVer: true });
+    addHandlers({
+      GetScriptVer({ meta }) {
+        setPrototypeOf(meta, null);
+        const el = document::querySelector('a.install-link');
+        if (el && el::getAttribute('data-script-name') === meta.name
+        && el::getAttribute('data-script-namespace') === meta.namespace) {
+          meta.publishVersion = el::getAttribute('data-script-version');
+          meta.publishDownloadURL = el::getAttribute('href');
+        }
+        return sendCmd('GetScriptVer', { meta });
+      },
+    });
diff --git a/src/injected/content/inject.js b/src/injected/content/inject.js
@@ -2 +2,3 @@ import bridge, { addHandlers, grantless } from './bridge';
-import { elemByTag, makeElem, nextTask, onElement, sendCmd } from './util';
+import {
+  elemByTag, getAttribute, makeElem, nextTask, onElement, querySelector, sendCmd,
+} from './util';
@@ -19,2 +20,0 @@ let nonce;
-let getAttribute;
-let querySelector;
@@ -160,4 +159,0 @@ export async function injectScripts(data, info, isXml) {
-  if (wasInjectableFF) {
-    getAttribute = Element[PROTO].getAttribute;
-    querySelector = document.querySelector;
-  }
diff --git a/src/injected/content/util.js b/src/injected/content/util.js
@@ -10,0 +11,2 @@ export const elemByTag = (tag, i) => getOwnProp(document::getElementsByTagName(t
+export const getAttribute = Element[PROTO].getAttribute;
+export const querySelector = document.querySelector;

@danielzgtg danielzgtg force-pushed the feat/greasyForkPublishButton branch from ad6c438 to e3569aa Compare May 31, 2026 19:19
Co-authored-by: tophf <tophff@gmail.com>
@danielzgtg

Copy link
Copy Markdown
Author

I really want it to open inside an iframe inside the editor tab

I started then stopped working on that. It could've allowed easy window.location instead of GetScriptVer, but I don't think the cookies will work. Here's a photoshopped mockup:

iframe maybe

judging by the HTTP response

At first, Greasy Fork will redirect to a login screen. I use GitHub and Google login. There's also preexisting cookies, cache partitioning, and CHIPS. Unless extensions are special, I think it might not work. Besides, you suggested "target=_blank" above.

Publish is sufficient as a label

Done

title="greasyfork.org" if you want

It can be added by any PR that adds OpenUserJS publish.

I think the label doesn't need to change to Push update

Please let us have this feature that Stylus has. I don't want to mistakenly publish my private potentially password-containing userscripts when I intended to update one of my public userscripts. Making the text the same will lead to desensitization to the seriousness of "Publish", exacerbated by Greasy Fork's layout and colours being the same on that page for both actions.

diff

Applied manually and rolled back src/injected/web/index.js. Somehow git apply threw error: patch fragment without header at line 3: @@ -7 +7 @@ import './tabs';.

@tophf

tophf commented May 31, 2026

Copy link
Copy Markdown
Member

Yeah an iframe won't work because the site sets X-Frame-Options header to 'sameorigin', and I guess Jason won't be too keen on us stripping it.

What about using const wnd = window.open() with the size parameter set to something like a half of screen size in each dimension, putting the form inside via wnd.document, and submitting it?

Making the text the same will lead to desensitization

That's a big claim, but I don't think that having different text will meaningfully reduce the possibility of a mistake. The most reliable solution is to not put sensitive info into publishable scripts in the first place e.g. maybe use GM storage instead. We can also add a confirmation on the first publish attempt within this editing session reminding about sensitive info.

@JasonBarnabe

Copy link
Copy Markdown

Yeah an iframe won't work because the site sets X-Frame-Options header to 'sameorigin', and I guess Jason won't be too keen on us stripping it.

If there's a change I can make that'll make your life easier without leaving the door wide open for bad actors, I'll do it. I'm not sure what origin I would need to allow for this to work for you.

@danielzgtg

Copy link
Copy Markdown
Author

What about using const wnd = window.open()

I brought back my original window.open code, simplified, then made it a new window as described:

popup prefill

not put sensitive info into publishable scripts in the first place e.g. maybe use GM storage instead

There are things like @match that must be in the userscript itself. Greasy Fork allows "Adult content", and user e.g. bible_thumping_republican_politician123 might be embarrassed if he forgot to switch browsers. Seeing "Publish" if he usually sees "Push update" will make it more likely that he will think twice.

@tophf

tophf commented Jun 1, 2026

Copy link
Copy Markdown
Member

Yeah looks fine.

things like @match that must be in the userscript itself

If we are still in the context of sensitive things that shouldn't be put into a greasyfork-connected script, such @match should be specified in the settings of the userscript, not in its code, because it's very easy to forget to remove it before publishing.

BTW, I wonder if authorization should be performed via browser.identity.launchWebAuthFlow the way we do it in Stylus...

@JasonBarnabe, a separate window seems fine as it's usually how it's done anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Button to publish to Greasy Fork / OpenUserJS

4 participants