diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 241ace475..cd57331ac 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,15 +1,15 @@ ## Contributing -This project uses [editor config](http://editorconfig.org/), please make sure to [download the plugin for your editor](http://editorconfig.org/#download) so that we stay consistent. Current ECMAScript 5 is implemented and supported at this time. This may change eventually to some ECMAScript 6 from the finalization that occurred in June of 2015. +This project uses [editor config](https://editorconfig.org/), please make sure to [download the plugin for your editor](https://editorconfig.org/#download) so that we stay consistent. Current ECMAScript 5 is implemented and supported at this time. This may change eventually to some ECMAScript 6 from the finalization that occurred in June of 2015. ### Creating a Local Environment #### Prerequisites -* [Git](http://git-scm.com/) -* [node.js](http://nodejs.org/) *(see [`./package.json`](https://github.com/OpenUserJs/OpenUserJS.org/blob/master/package.json) engines for specific requirements)* -* [MongoDB](http://www.mongodb.org/) (Optional. The project is preconfigured to use a dev DB on [MongoLab](https://mongolab.com/).) +* [Git](https://git-scm.com/) +* [node.js](https://nodejs.org/) *(see [`./package.json`](https://github.com/OpenUserJs/OpenUserJS.org/blob/master/package.json) engines for specific requirements)* +* [MongoDB](https://www.mongodb.org/) *(Required: See [overall instructions here](https://docs.mongodb.com/manual/installation/) or [Community Edition instructions](https://docs.mongodb.com/manual/administration/install-community/))* #### GitHub Fork Setup @@ -32,11 +32,19 @@ This project uses [editor config](http://editorconfig.org/), please make sure to #### Configuration 1. Navigate to https://github.com/settings/applications and register a new OAuth application, saving the Client ID and Secret. To ensure GitHub OAuth authentication will work the "Authorization callback URL" value must exactly match `AUTH_CALLBACK_BASE_URL` (see below, e.g. http://localhost:8080). -2. Open a [MongoDB shell](http://docs.mongodb.org/manual/tutorial/getting-started-with-the-mongo-shell/) and run the following (replacing "your_GitHub_client_ID" and "your_GitHub_secret") to create an "oujs_dev" database with a "strategies" collection containing your application instance's GitHub OAuth information. - * `use oujs_dev` +2. Open a [MongoDB shell](https://docs.mongodb.com/manual/mongo/) and run the following (replacing "your_GitHub_client_ID" and "your_GitHub_secret") to create an "openuserjs_devel" database with a "strategies" collection containing your application instance's GitHub OAuth information. + * `use openuserjs_devel` * `db.createCollection("strategies")` * `db.strategies.insert({id: "your_GitHub_client_ID", key: "your_GitHub_secret", name: "github", display: "GitHub"})` -3. Edit `models/settings.json`, setting your desired session secret, [MongoDB connection string](http://docs.mongodb.org/manual/reference/connection-string/) (if using your own MongoDB instance), etc. +3. Edit `models/settings.json`, setting your desired session secret, [MongoDB connection string](https://docs.mongodb.com/manual/reference/connection-string/) (if using your own MongoDB instance), etc. +4. Depending on how you installed MongoDB you may need to set an environment variable in your CLI: + +Linux Example: +~/.bashrc +``` console +export CONNECT_STRING=mongodb://127.0.0.1:27017/openuserjs_devel +``` +5. You may import a “dirty” database using `mongorestore --gzip --db openuserjs_devel --archive=./dev/devDBdirty.gz` from the projects home. #### Running the Application @@ -70,6 +78,10 @@ To contribute code to OpenUserJS.org the following process should generally be u The following is a brief list of **some** of the labels used on the project and is used to establish teamwork. Not everyone has permission to set these and usually will be set by someone, unless expressly prohibited, either when an Issue or Pull Request *(PR)* is created or after an Issue is reported: +##### SOFT BLOCKING +Only the establishing owner and in extreme cases the active maintainer of the project may add this. Removal is usually done by the active maintainer and above. Recommendations by other contributors and collaborators are always accepted to have this put on or removed. This label means that merging unrelated or non-bug fix PRs will be put on hold until this label has been removed. Documentation fixes are always welcome by the active maintainer. + + ##### BLOCKING Only the establishing owner and in extreme cases the active maintainer of the project may add this. Removal is done by the establishing owner. Recommendations by other contributors and collaborators are always accepted to have this put on or removed. This label means that merging unrelated or non-bug fix PRs will be put on hold until this label has been removed. Documentation fixes are always welcome by the Active Maintainer. diff --git a/.github/ISSUE_TEMPLATE/01_feature_request.yml b/.github/ISSUE_TEMPLATE/01_feature_request.yml new file mode 100644 index 000000000..e6bef7081 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_feature_request.yml @@ -0,0 +1,29 @@ +name: Feature Request +description: Something we **DO NOT** already have implemented to the best of your knowledge but you would like to see. (Please search existing issues and milestones first.) +labels: ["feature"] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill this out. + - type: textarea + id: what-is-missing + attributes: + label: What is missing? + description: Describe your feature idea. + placeholder: Start typing here. + validations: + required: true + - type: textarea + id: why + attributes: + label: Why? + description: Describe the problem you are facing. + placeholder: Continue typing here. + validations: + required: true + - type: textarea + id: alt + attributes: + label: Alternatives you have tried. + description: Describe any workarounds you tried so far and how they worked for you. diff --git a/.github/ISSUE_TEMPLATE/02_enhancement_request.yml b/.github/ISSUE_TEMPLATE/02_enhancement_request.yml new file mode 100644 index 000000000..2707fc776 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_enhancement_request.yml @@ -0,0 +1,29 @@ +name: Enhancement Request +description: Something we **DO** have implemented already but needs improvement upon to the best of your knowledge. (Please search existing issues and milestones first.) +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill this out. + - type: textarea + id: what-is-missing + attributes: + label: What is missing? + description: Describe your enhancement idea. + placeholder: Start typing here. + validations: + required: true + - type: textarea + id: why + attributes: + label: Why? + description: Describe the problem you are facing. + placeholder: Continue typing here. + validations: + required: true + - type: textarea + id: alt + attributes: + label: Alternatives you have tried. + description: Describe any workarounds you tried so far and how they worked for you. diff --git a/.github/ISSUE_TEMPLATE/03_possible_bug.yml b/.github/ISSUE_TEMPLATE/03_possible_bug.yml new file mode 100644 index 000000000..5ea8e9dbb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_possible_bug.yml @@ -0,0 +1,31 @@ +name: Bug investigation +description: | + If something isn't working as expected. + (Status codes below 500 are usually a user error and not a server error. i.e. Usually not a bug. Please search existing issues and milestones first.) +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to fill this out. + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe the problem and how to reproduce it. Add screenshots or a link to your repository if helpful. + placeholder: Start typing here. + validations: + required: true + - type: textarea + id: what-expected + attributes: + label: What did you expect to happen? + description: Describe what you expected to happen instead. + placeholder: Continue typing here. + validations: + required: true + - type: textarea + id: solutions-tried + attributes: + label: What the problem might be? + description: If you have an idea where the bug might lie, please share here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..d20b72c2c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Community-driven help + url: https://openuserjs.org/discuss + about: Do you have a question? Start here. diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 000000000..f158d7111 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,24 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + discussions: write + +concurrency: + group: lock-threads + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-inactive-days: '1' + process-only: 'issues' diff --git a/.gitignore b/.gitignore index d168ab0dd..ad219c646 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ lib-cov *.out *.pid *.gz +!dev/devDBclean.gz +!dev/devDBdirty.gz pids logs diff --git a/README.md b/README.md index 55857b8f4..b836806a5 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ The home of Free and Open Source Software (FOSS) user scripts. Built using Node. Repository | Reference | Recent Version :--- | :---: | :--- -[nodejs][nodeGHUrl] | [Documentation][nodejsDOCUrl] | Current release schedule [¹][nodejsGHReleasesUrl] [CHANGELOG][nodejsReleasesUrl] -[npm][npmGHUrl] | [Documentation][npmDOCUrl] | [![npm][npmNPMVersionImage]][npmNPMUrl] [CHANGELOG][npmGHReleasesUrl] +[nodejs][nodeGHUrl] | [Documentation][nodejsDOCUrl] | Current release schedule [¹][nodejsGHReleasesUrl] [RELEASES][nodejsReleasesUrl] [²][nodejsDownloadsUrl] [³][nodejsNvmDownload] +[npm][npmGHUrl] | [Documentation][npmDOCUrl] | [![npm][npmNPMVersionImage]][npmNPMUrl] [RELEASES][npmGHReleasesUrl] ### Contributing @@ -15,30 +15,33 @@ Repository | Reference | Recent Version ### Dependencies #### Dispersed -These also may use [shields.io][shieldsHomepage] where applicable for more explicit and up to date results. +These also may use [a badge provider][badgeProvider] where applicable for potentialliy more up to date results. ##### Dynamic Repository | Reference | Recent Version --- | --- | --- -[ace-builds][ace-buildsGHUrl] [¹][aceGHUrl] | [Documentation][ace-buildsDOCUrl] [¹][aceDOCUrl] | [1.4.9][ace-buildsGHHASHUrl] [RELEASES][ace-buildsGHRELEASESUrl] +[ace-builds][ace-buildsGHUrl] [¹][aceGHUrl] | [Documentation][ace-buildsDOCUrl] [¹][aceDOCUrl] | [![NPM version][ace-buildsNPMVersionImage]][ace-buildsNPMUrl] +[animate.css][animate.cssGHUrl] | [Documentation][animate.cssDOCUrl] | [![NPM version][animate.cssNPMVersionImage]][animate.cssNPMUrl] [ansi-colors][ansi-colorsGHUrl] | [Documentation][ansi-colorsDOCUrl] | [![NPM version][ansi-colorsNPMVersionImage]][ansi-colorsNPMUrl] [async][asyncGHUrl] | [Documentation][asyncDOCUrl] | [![NPM version][asyncNPMVersionImage]][asyncNPMUrl] [aws-sdk][aws-sdkGHUrl] | [Documentation][aws-sdkDOCUrl] | [![NPM version][aws-sdkNPMVersionImage]][aws-sdkNPMUrl] -[base62][base62GHUrl] | [Documentation][base62DOCUrl] | [![NPM version][base62NPMVersionImage]][base62NPMUrl] [body-parser][body-parserGHUrl] | [Documentation][body-parserDOCUrl] | [![NPM version][body-parserNPMVersionImage]][body-parserNPMUrl] [bootstrap][bootstrapGHUrl] | [Documentation][bootstrapDOCUrl] | [![NPM version][bootstrapNPMVersionImage]][bootstrapNPMUrl] -[bootstrap-markdown][bootstrap-markdownGHUrl] | [Documentation][bootstrap-markdownDOCUrl] | [![NPM version][bootstrap-markdownNPMVersionImage]][bootstrap-markdownNPMUrl] +[bootstrap-markdown][bootstrap-markdownGHUrl]
⋔ [`marked4.x`][bootstrap-markdownGHUrlForkUrl] | [Documentation][bootstrap-markdownDOCUrl] | [![NPM version][bootstrap-markdownNPMVersionImage]][bootstrap-markdownNPMUrl] [clipboard][clipboardGHUrl] | [Documentation][clipboardDOCUrl] | [![NPM version][clipboardNPMVersionImage]][clipboardNPMUrl] [compression][compressionGHUrl] | [Documentation][compressionDOCUrl] | [![NPM version][compressionNPMVersionImage]][compressionNPMUrl] [connect-mongo][connect-mongoGHUrl] | [Documentation][connect-mongoDOCUrl] | [![NPM version][connect-mongoNPMVersionImage]][connect-mongoNPMUrl] [diff][diffGHUrl] | [Documentation][diffDOCUrl] | [![NPM version][diffNPMVersionImage]][diffNPMUrl] [express][expressGHUrl] | [Documentation][expressDOCUrl] | [![NPM version][expressNPMVersionImage]][expressNPMUrl] +[express-hcaptcha][express-hcaptchaGHUrl]
⋔ [`forkUpdate`][express-hcaptchaGHUrlForkUrl] | [Documentation][express-hcaptchaDOCUrl] | [![NPM version][express-hcaptchaNPMVersionImage]][express-hcaptchaNPMUrl] [express-minify][express-minifyGHUrl] | [Documentation][express-minifyDOCUrl] | [![NPM version][express-minifyNPMVersionImage]][express-minifyNPMUrl] [express-rate-limit][express-rate-limitGHUrl] | [Documentation][express-rate-limitDOCUrl] | [![NPM version][express-rate-limitNPMVersionImage]][express-rate-limitNPMUrl] [express-session][express-sessionGHUrl] | [Documentation][express-sessionDOCUrl] | [![NPM version][express-sessionNPMVersionImage]][express-sessionNPMUrl] +[express-svg-captcha][express-svg-captchaGHUrl] | [Documentation][express-svg-captchaDOCUrl] | [![NPM version][express-svg-captchaNPMVersionImage]][express-svg-captchaNPMUrl] [font-awesome][font-awesomeGHUrl] | [Documentation][font-awesomeDOCUrl] | [![NPM version][font-awesomeNPMVersionImage]][font-awesomeNPMUrl] [formidable][formidableGHUrl] | [Documentation][formidableDOCUrl] | [![NPM version][formidableNPMVersionImage]][formidableNPMUrl] [git-rev][git-revGHUrl] | [Documentation][git-revDOCUrl] | [![NPM version][git-revNPMVersionImage]][git-revNPMUrl] +[git-rev-sync][git-rev-syncGHUrl] | [Documentation][git-rev-syncDOCUrl] | [![NPM version][git-rev-syncNPMVersionImage]][git-rev-syncNPMUrl] [highlight.js][highlight.jsGHUrl] | [Documentation][highlight.jsDOCUrl][ᴸᴬᴺᴳ][highlight.jsLANGUrl] | [![NPM version][highlight.jsNPMVersionImage]][highlight.jsNPMUrl] [image-size][image-sizeGHUrl] | [Documentation][image-sizeDOCUrl] | [![NPM version][image-sizeNPMVersionImage]][image-sizeNPMUrl] [ip-range-check][ip-range-checkGHUrl] | [Documentation][ip-range-checkDOCUrl] | [![NPM version][ip-range-checkNPMVersionImage]][ip-range-checkNPMUrl] @@ -48,6 +51,7 @@ Repository | Reference | Recent Version [kerberos][kerberosGHUrl] | [Documentation][kerberosDOCUrl] | [![NPM version][kerberosNPMVersionImage]][kerberosNPMUrl] [less-middleware][less-middlewareGHUrl] [¹][lessGHUrl] | [Documentation][less-middlewareDOCUrl] [¹][lessDOCUrl] | [![NPM version][less-middlewareNPMVersionImage]][less-middlewareNPMUrl] [marked][markedGHUrl] | [Documentation][markedDOCUrl] | [![NPM version][markedNPMVersionImage]][markedNPMUrl] +[marked-highlight][marked-highlightGHUrl] | [Documentation][marked-highlightDOCUrl] | [![NPM version][marked-highlightNPMVersionImage]][marked-highlightNPMUrl] [media-type][media-typeGHUrl] | [Documentation][media-typeDOCUrl] | [![NPM version][media-typeNPMVersionImage]][media-typeNPMUrl] [method-override][method-overrideGHUrl] | [Documentation][method-overrideDOCUrl] | [![NPM version][method-overrideNPMVersionImage]][method-overrideNPMUrl] [mime-db][mime-dbGHUrl] | [Documentation][mime-dbDOCUrl] | [![NPM version][mime-dbNPMVersionImage]][mime-dbNPMUrl] @@ -59,15 +63,12 @@ Repository | Reference | Recent Version [mu2][mu2GHUrl] | [Documentation][mu2DOCUrl] | [![NPM version][mu2NPMVersionImage]][mu2NPMUrl] [octicons][octiconsGHUrl] | [Documentation][octiconsDOCUrl] | [![NPM version][octiconsNPMVersionImage]][octiconsNPMUrl] [passport][passportGHUrl] | [Documentation][passportDOCUrl] | [![NPM version][passportNPMVersionImage]][passportNPMUrl] -[passport-facebook][passport-facebookGHUrl] | [Documentation][passport-facebookDOCUrl] | [![NPM version][passport-facebookNPMVersionImage]][passport-facebookNPMUrl] ![OAuth2][oauth2Logo] [passport-github][passport-githubGHUrl] | [Documentation][passport-githubDOCUrl] | [![NPM version][passport-githubNPMVersionImage]][passport-githubNPMUrl] ![OAuth2][oauth2Logo] [passport-gitlab2][passport-gitlab2GHUrl] | [Documentation][passport-gitlab2DOCUrl] | [![NPM version][passport-gitlab2NPMVersionImage]][passport-gitlab2NPMUrl] ![OAuth2][oauth2Logo] [passport-google-oauth2][passport-google-oauth2GHUrl] | [Documentation][passport-google-oauth2DOCUrl] | [![NPM version][passport-google-oauth2NPMVersionImage]][passport-google-oauth2NPMUrl] ![OAuth2][oauth2Logo] [passport-imgur][passport-imgurGHUrl] | [Documentation][passport-imgurDOCUrl] | [![NPM version][passport-imgurNPMVersionImage]][passport-imgurNPMUrl] ![OAuth2][oauth2Logo] ![oauth][oauthLogo] -[passport-reddit][passport-redditGHUrl] | [Documentation][passport-redditDOCUrl] | [![NPM version][passport-redditNPMVersionImage]][passport-redditNPMUrl] ![OAuth2][oauth2Logo] -[passport-steam][passport-steamGHUrl]
⋔ [`OpenID2`][passport-steamGHOpenIDUrl] | [Documentation][passport-steamDOCUrl] | [![NPM version][passport-steamNPMVersionImage]][passport-steamNPMUrl] ![OpenID][openidLogo] [⋔][passport-openid] -[passport-twitter][passport-twitterGHUrl] | [Documentation][passport-twitterDOCUrl] | [![NPM version][passport-twitterNPMVersionImage]][passport-twitterNPMUrl] ![oauth1][oauth1Logo] -[passport-yahoo][passport-yahooGHUrl]
⋔ [`OpenID2`][passport-yahooGHOpenIDUrl] | [Documentation][passport-yahooDOCUrl] | [![NPM version][passport-yahooNPMVersionImage]][passport-yahooNPMUrl] ![OpenID][openidLogo] [⋔][passport-openid] +[passport-reddit][passport-redditGHUrl] [
⋔ `536b656c6c79/passport-reddit-commonJS`][passport-reddit-commonjsGHUrl] | [Documentation][passport-redditDOCUrl]
[⋔][passport-reddit-commonjsDOCUrl] | [![NPM version][passport-redditNPMVersionImage]][passport-redditNPMUrl] ![OAuth2][oauth2Logo]
[⋔ ![NPM version][passport-reddit-commonjsNPMVersionImage]][passport-reddit-commonjsNPMUrl] +[passport-steam][passport-steamGHUrl] [
⋔ `OpenID2`][passport-steamGHOpenIDUrl] | [Documentation][passport-steamDOCUrl] | [![NPM version][passport-steamNPMVersionImage]][passport-steamNPMUrl] ![OpenID][openidLogo]
[⋔][passport-openid] [pegjs][pegjsGHUrl] | [Documentation][pegjsDOCUrl] | [![NPM version][pegjsNPMVersionImage]][pegjsNPMUrl] [rate-limit-mongo][rate-limit-mongoGHUrl] | [Documentation][rate-limit-mongoDOCUrl] | [![NPM version][rate-limit-mongoNPMVersionImage]][rate-limit-mongoNPMUrl] [remark][remarkGHUrl] | [Documentation][remarkDOCUrl] | [![NPM version][remarkNPMVersionImage]][remarkNPMUrl] @@ -83,11 +84,11 @@ Repository | Reference | Recent Version [strip-markdown][strip-markdownGHUrl] | [Documentation][strip-markdownDOCUrl] | [![NPM version][strip-markdownNPMVersionImage]][strip-markdownNPMUrl] [terser][terserGHUrl] | [Documentation][terserDOCUrl] | [![NPM version][terserNPMVersionImage]][terserNPMUrl] [toobusy-js][toobusy-jsGHUrl]
⋔ [`harmony`][toobusy-jsGHUrlHarmonyUrl] | [Documentation][toobusy-jsDOCUrl] | [![NPM version][toobusy-jsNPMVersionImage]][toobusy-jsNPMUrl] -[underscore][underscoreGHUrl] | [Documentation][underscoreDOCUrl] | [![NPM version][underscoreNPMVersionImage]][underscoreNPMUrl] +[underscore][underscoreGHUrl] | [Documentation][underscoreDOCUrl] [Δ][underscoreDOCCLUrl] | [![NPM version][underscoreNPMVersionImage]][underscoreNPMUrl] [useragent][useragentGHUrl] | [Documentation][useragentDOCUrl] | [![NPM version][useragentNPMVersionImage]][useragentNPMUrl] +[@octokit/auth-oauth-app][auth-oauth-appGHUrl] | [Documentation][auth-oauth-appDOCUrl] | [![NPM version][auth-oauth-appNPMVersionImage]][auth-oauth-appNPMUrl] [@octokit/rest ᶠᵏᵃ ᵍᶦᵗʰᵘᵇ][githubGHUrl] | [Documentation][githubDOCUrl] | [![NPM version][githubNPMVersionImage]][githubNPMUrl] - ##### Static Repository | Reference | Recent Version | Stored @@ -99,401 +100,408 @@ Repository | Reference | Recent Version | Stored Repository | Reference | Recent Version | Referenced --- | --- | --- | --- -[Google Analytics][gaCFGUrl] | [Documentation][gaDOCUrl] | Client-side from GA | [gaCDNUrl] #### Aggregate -[![Using David][davidImageViaShields]][davidReport] - -Outdated dependencies list can also be achieved with `$ npm outdated` +Outdated dependencies list can be achieved with `$ npm outdated` from the terminal and alternatively `$ npm show packagename versions`. [nodeGHUrl]: https://github.com/nodejs/node [nodejsGHReleasesUrl]: https://github.com/nodejs/Release/#release-schedule -[nodejsReleasesUrl]: https://nodejs.org/download/release/ -[nodejsDOCUrl]: http://nodejs.org/documentation/ +[nodejsReleasesUrl]: https://nodejs.org/blog/release +[nodejsDownloadsUrl]: https://nodejs.org/download/release/ +[nodejsDOCUrl]: https://nodejs.org/documentation/ +[nodejsNvmDownload]: https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating [npmNPMUrl]: https://www.npmjs.com/package/npm -[npmNPMVersionImage]: http://img.shields.io/npm/v/npm.svg +[npmNPMVersionImage]: https://badgen.net/npm/v/npm?cache=86400 [npmGHReleasesUrl]: https://github.com/npm/cli/releases [npmGHUrl]: https://github.com/npm/cli [npmDOCUrl]: https://github.com/npm/cli/blob/latest/README.md -[davidImageViaShields]: http://img.shields.io/david/openuserjs/openuserjs.org.svg?style=flat -[davidReport]: https://david-dm.org/OpenUserJS/OpenUserJS.org - -[shieldsHomepage]: http://shields.io/ +[badgeProvider]: https://badgen.net/ [ace-buildsGHUrl]: https://github.com/ajaxorg/ace-builds/tree/master/src -[ace-buildsGHHASHUrl]: https://github.com/ajaxorg/ace-builds/tree/bd7ce25 -[ace-buildsGHRELEASESUrl]: https://github.com/ajaxorg/ace-builds/releases [ace-buildsDOCUrl]: https://github.com/ajaxorg/ace-builds/blob/master/README.md +[ace-buildsNPMUrl]: https://www.npmjs.com/package/ace-builds +[ace-buildsNPMVersionImage]: https://badgen.net/npm/v/ace-builds?cache=86400 [aceGHUrl]: https://github.com/ajaxorg/ace "ace" -[aceDOCUrl]: http://ace.c9.io/#nav=api "ace" +[aceDOCUrl]: https://ace.c9.io/#nav=api "ace" + +[animate.cssGHUrl]: https://github.com/animate-css/animate.css +[animate.cssDOCUrl]: https://animate.style/ +[animate.cssNPMUrl]: https://www.npmjs.com/package/animate.css +[animate.cssNPMVersionImage]: https://badgen.net/npm/v/animate.css?cache=86400 [ansi-colorsGHUrl]: https://github.com/doowb/ansi-colors [ansi-colorsDOCUrl]: https://github.com/doowb/ansi-colors/blob/master/README.md [ansi-colorsNPMUrl]: https://www.npmjs.com/package/ansi-colors -[ansi-colorsNPMVersionImage]: https://img.shields.io/npm/v/ansi-colors.svg?style=flat +[ansi-colorsNPMVersionImage]: https://badgen.net/npm/v/ansi-colors?cache=86400 [asyncGHUrl]: https://github.com/caolan/async [asyncDOCUrl]: https://github.com/caolan/async/blob/master/README.md [asyncNPMUrl]: https://www.npmjs.com/package/async -[asyncNPMVersionImage]: https://img.shields.io/npm/v/async.svg?style=flat +[asyncNPMVersionImage]: https://badgen.net/npm/v/async?cache=86400 [aws-sdkGHUrl]: https://github.com/aws/aws-sdk-js [aws-sdkDOCUrl]: https://github.com/aws/aws-sdk-js/blob/master/README.md [aws-sdkNPMUrl]: https://www.npmjs.com/package/aws-sdk -[aws-sdkNPMVersionImage]: https://img.shields.io/npm/v/aws-sdk.svg?style=flat - -[base62GHUrl]: https://github.com/base62/base62.js -[base62DOCUrl]: https://github.com/base62/base62.js/blob/master/Readme.md -[base62NPMUrl]: https://www.npmjs.com/package/base62 -[base62NPMVersionImage]: https://img.shields.io/npm/v/base62.svg?style=flat +[aws-sdkNPMVersionImage]: https://badgen.net/npm/v/aws-sdk?cache=86400 [body-parserGHUrl]: https://github.com/expressjs/body-parser [body-parserDOCUrl]: https://github.com/expressjs/body-parser/blob/master/README.md [body-parserNPMUrl]: https://www.npmjs.com/package/body-parser -[body-parserNPMVersionImage]: https://img.shields.io/npm/v/body-parser.svg?style=flat +[body-parserNPMVersionImage]: https://badgen.net/npm/v/body-parser?cache=86400 -[bootstrapUrl]: http://getbootstrap.com/ +[bootstrapUrl]: https://getbootstrap.com/ [bootstrapGHUrl]: https://github.com/twbs/bootstrap -[bootstrapDOCUrl]: http://getbootstrap.com/components/ +[bootstrapDOCUrl]: https://getbootstrap.com/components/ [bootstrapNPMUrl]: https://www.npmjs.com/package/bootstrap -[bootstrapNPMVersionImage]: https://img.shields.io/npm/v/bootstrap.svg?style=flat +[bootstrapNPMVersionImage]: https://badgen.net/npm/v/bootstrap?cache=86400 -[bootstrap-markdownGHUrl]: https://github.com/toopay/bootstrap-markdown -[bootstrap-markdownDOCUrl]: http://toopay.github.io/bootstrap-markdown/ +[bootstrap-markdownGHUrl]: https://github.com/refactory-id/bootstrap-markdown +[bootstrap-markdownGHUrlForkUrl]: https://github.com/OpenUserJS/bootstrap-markdown/tree/marked4.x +[bootstrap-markdownDOCUrl]: https://refactory-id.github.io/bootstrap-markdown/ [bootstrap-markdownNPMUrl]: https://www.npmjs.com/package/bootstrap-markdown -[bootstrap-markdownNPMVersionImage]: https://img.shields.io/npm/v/bootstrap-markdown.svg?style=flat +[bootstrap-markdownNPMVersionImage]: https://badgen.net/npm/v/bootstrap-markdown?cache=86400 [clipboardGHUrl]: https://github.com/zenorocha/clipboard.js [clipboardDOCUrl]: https://github.com/zenorocha/clipboard.js/blob/master/readme.md [clipboardNPMUrl]: https://www.npmjs.com/package/clipboard -[clipboardNPMVersionImage]: https://img.shields.io/npm/v/clipboard.svg?style=flat +[clipboardNPMVersionImage]: https://badgen.net/npm/v/clipboard?cache=86400 [compressionGHUrl]: https://github.com/expressjs/compression [compressionDOCUrl]: https://github.com/expressjs/compression/blob/master/README.md [compressionNPMUrl]: https://www.npmjs.com/package/compression -[compressionNPMVersionImage]: https://img.shields.io/npm/v/compression.svg?style=flat +[compressionNPMVersionImage]: https://badgen.net/npm/v/compression?cache=86400 -[connect-mongoGHUrl]: https://github.com/kcbanner/connect-mongo -[connect-mongoDOCUrl]: https://github.com/kcbanner/connect-mongo/blob/master/README.md +[connect-mongoGHUrl]: https://github.com/jdesboeufs/connect-mongo +[connect-mongoDOCUrl]: https://github.com/jdesboeufs/connect-mongo/blob/master/README.md [connect-mongoNPMUrl]: https://www.npmjs.com/package/connect-mongo -[connect-mongoNPMVersionImage]: https://img.shields.io/npm/v/connect-mongo.svg?style=flat +[connect-mongoNPMVersionImage]: https://badgen.net/npm/v/connect-mongo?cache=86400 [diffGHUrl]: https://github.com/kpdecker/jsdiff [diffDOCUrl]: https://github.com/kpdecker/jsdiff/blob/master/README.md [diffNPMUrl]: https://www.npmjs.com/package/diff -[diffNPMVersionImage]: https://img.shields.io/npm/v/diff.svg?style=flat +[diffNPMVersionImage]: https://badgen.net/npm/v/diff?cache=86400 [expressGHUrl]: https://github.com/expressjs/express -[expressDOCUrl]: http://expressjs.com/ +[expressDOCUrl]: https://expressjs.com/ [expressNPMUrl]: https://www.npmjs.com/package/express -[expressNPMVersionImage]: https://img.shields.io/npm/v/express.svg?style=flat +[expressNPMVersionImage]: https://badgen.net/npm/v/express?cache=86400 + +[express-hcaptchaGHUrl]: https://github.com/vastus/express-hcaptcha +[express-hcaptchaGHUrlForkUrl]: https://github.com/OpenUserJS/express-hcaptcha/tree/forkUpdate +[express-hcaptchaDOCUrl]: https://github.com/vastus/express-hcaptcha/blob/master/README.md +[express-hcaptchaNPMUrl]: https://www.npmjs.com/package/express-hcaptcha +[express-hcaptchaNPMVersionImage]: https://badgen.net/npm/v/express-hcaptcha?cache=86400 [express-minifyGHUrl]: https://github.com/breeswish/express-minify [express-minifyDOCUrl]: https://github.com/breeswish/express-minify/blob/master/README.md [express-minifyNPMUrl]: https://www.npmjs.com/package/express-minify -[express-minifyNPMVersionImage]: https://img.shields.io/npm/v/express-minify.svg?style=flat +[express-minifyNPMVersionImage]: https://badgen.net/npm/v/express-minify?cache=86400 -[express-rate-limitGHUrl]: https://github.com/nfriedly/express-rate-limit -[express-rate-limitDOCUrl]: https://github.com/nfriedly/express-rate-limit/blob/master/README.md +[express-rate-limitGHUrl]: https://github.com/express-rate-limit/express-rate-limit +[express-rate-limitDOCUrl]: https://github.com/express-rate-limit/express-rate-limit/blob/main/readme.md [express-rate-limitNPMUrl]: https://www.npmjs.com/package/express-rate-limit -[express-rate-limitNPMVersionImage]: https://img.shields.io/npm/v/express-rate-limit.svg?style=flat +[express-rate-limitNPMVersionImage]: https://badgen.net/npm/v/express-rate-limit?cache=86400 [express-sessionGHUrl]: https://github.com/expressjs/session [express-sessionDOCUrl]: https://github.com/expressjs/session/blob/master/README.md [express-sessionNPMUrl]: https://www.npmjs.com/package/express-session -[express-sessionNPMVersionImage]: https://img.shields.io/npm/v/express-session.svg?style=flat +[express-sessionNPMVersionImage]: https://badgen.net/npm/v/express-session?cache=86400 + +[express-svg-captchaGHUrl]: https://github.com/cmd430/express-svg-captcha +[express-svg-captchaDOCUrl]: https://github.com/cmd430/express-svg-captcha/blob/master/README.md +[express-svg-captchaNPMUrl]: https://www.npmjs.com/package/express-svg-captcha +[express-svg-captchaNPMVersionImage]: https://badgen.net/npm/v/express-svg-captcha?cache=86400 [font-awesomeGHUrl]: https://github.com/FortAwesome/Font-Awesome -[font-awesomeDOCUrl]: http://fontawesome.io/ +[font-awesomeDOCUrl]: https://fontawesome.com/ [font-awesomeNPMUrl]: https://www.npmjs.com/package/font-awesome -[font-awesomeNPMVersionImage]: https://img.shields.io/npm/v/font-awesome.svg?style=flat +[font-awesomeNPMVersionImage]: https://badgen.net/npm/v/font-awesome?cache=86400 -[formidableGHUrl]: https://github.com/felixge/node-formidable -[formidableDOCUrl]: https://github.com/felixge/node-formidable/blob/master/Readme.md +[formidableGHUrl]: https://github.com/node-formidable/formidable +[formidableDOCUrl]: https://github.com/node-formidable/formidable/blob/master/README.md [formidableNPMUrl]: https://www.npmjs.com/package/formidable -[formidableNPMVersionImage]: https://img.shields.io/npm/v/formidable.svg?style=flat - -[githubGHUrl]: https://github.com/octokit/rest.js -[githubDOCUrl]: https://github.com/octokit/rest.js/blob/master/README.md -[githubNPMUrl]: https://www.npmjs.com/package/@octokit/rest -[githubNPMVersionImage]: https://img.shields.io/npm/v/@octokit/rest.svg?style=flat +[formidableNPMVersionImage]: https://badgen.net/npm/v/formidable?cache=86400 [git-revGHUrl]: https://github.com/tblobaum/git-rev [git-revDOCUrl]: https://github.com/tblobaum/git-rev/blob/master/README.md [git-revNPMUrl]: https://www.npmjs.com/package/git-rev -[git-revNPMVersionImage]: https://img.shields.io/npm/v/git-rev.svg?style=flat +[git-revNPMVersionImage]: https://badgen.net/npm/v/git-rev?cache=86400 -[highlight.jsGHUrl]: https://github.com/isagalaev/highlight.js -[highlight.jsDOCUrl]: http://highlightjs.readthedocs.org +[git-rev-syncGHUrl]: https://github.com/kurttheviking/git-rev-sync-js +[git-rev-syncDOCUrl]: https://github.com/kurttheviking/git-rev-sync-js/blob/master/README.md +[git-rev-syncNPMUrl]: https://www.npmjs.com/package/git-rev-sync +[git-rev-syncNPMVersionImage]: https://badgen.net/npm/v/git-rev-sync?cache=86400 + +[highlight.jsGHUrl]: https://github.com/highlightjs/highlight.js +[highlight.jsDOCUrl]: https://highlightjs.readthedocs.io [highlight.jsNPMUrl]: https://www.npmjs.com/package/highlight.js -[highlight.jsNPMVersionImage]: https://img.shields.io/npm/v/highlight.js.svg?style=flat -[highlight.jsLANGUrl]: https://github.com/highlightjs/highlight.js#getting-started +[highlight.jsNPMVersionImage]: https://badgen.net/npm/v/highlight.js?cache=86400 +[highlight.jsLANGUrl]: https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md [image-sizeNPMUrl]: https://www.npmjs.com/package/image-size -[image-sizeNPMVersionImage]: https://img.shields.io/npm/v/image-size.svg?style=flat +[image-sizeNPMVersionImage]: https://badgen.net/npm/v/image-size?cache=86400 [image-sizeGHUrl]: https://github.com/image-size/image-size [image-sizeDOCUrl]: https://github.com/image-size/image-size/blob/master/Readme.md [ip-range-checkGHUrl]: https://github.com/danielcompton/ip-range-check [ip-range-checkDOCUrl]: https://github.com/danielcompton/ip-range-check/blob/master/README.md [ip-range-checkNPMUrl]: https://www.npmjs.com/package/ip-range-check -[ip-range-checkNPMVersionImage]: https://img.shields.io/npm/v/ip-range-check.svg?style=flat +[ip-range-checkNPMVersionImage]: https://badgen.net/npm/v/ip-range-check?cache=86400 [jQueryNPMUrl]: https://www.npmjs.com/package/jquery -[jQueryNPMVersionImage]: https://img.shields.io/npm/v/jquery.svg?style=flat +[jQueryNPMVersionImage]: https://badgen.net/npm/v/jquery?cache=86400 [jQueryGHUrl]: https://github.com/jquery/jquery -[jQueryUrl]: http://jquery.com/ -[jQueryDOCUrl]: http://api.jquery.com/ +[jQueryUrl]: https://jquery.com/ +[jQueryDOCUrl]: https://api.jquery.com/ [js-beautifyNPMUrl]: https://www.npmjs.com/package/js-beautify -[js-beautifyNPMVersionImage]: https://img.shields.io/npm/v/js-beautify.svg?style=flat -[js-beautifyGHUrl]: https://github.com/beautify-web/js-beautify -[js-beautifyDOCUrl]: https://github.com/beautify-web/js-beautify/blob/master/README.md +[js-beautifyNPMVersionImage]: https://badgen.net/npm/v/js-beautify?cache=86400 +[js-beautifyGHUrl]: https://github.com/beautifier/js-beautify +[js-beautifyDOCUrl]: https://github.com/beautifier/js-beautify/blob/main/README.md [jsdomNPMUrl]: https://www.npmjs.com/package/jsdom -[jsdomNPMVersionImage]: https://img.shields.io/npm/v/jsdom.svg?style=flat -[jsdomGHUrl]: https://github.com/tmpvar/jsdom -[jsdomDOCUrl]: https://github.com/tmpvar/jsdom/blob/master/README.md +[jsdomNPMVersionImage]: https://badgen.net/npm/v/jsdom?cache=86400 +[jsdomGHUrl]: https://github.com/jsdom/jsdom +[jsdomDOCUrl]: https://github.com/jsdom/jsdom/blob/master/README.md [kerberosNPMUrl]: https://www.npmjs.com/package/kerberos -[kerberosNPMVersionImage]: https://img.shields.io/npm/v/kerberos.svg?style=flat -[kerberosGHUrl]: https://github.com/christkv/kerberos -[kerberosDOCUrl]: https://github.com/christkv/kerberos/blob/master/README.md +[kerberosNPMVersionImage]: https://badgen.net/npm/v/kerberos?cache=86400 +[kerberosGHUrl]: https://github.com/mongodb-js/kerberos +[kerberosDOCUrl]: https://github.com/mongodb-js/kerberos/blob/master/README.md [less-middlewareGHUrl]: https://github.com/emberfeather/less.js-middleware [less-middlewareDOCUrl]: https://github.com/emberfeather/less.js-middleware/blob/master/readme.md [less-middlewareNPMUrl]: https://www.npmjs.com/package/less-middleware -[less-middlewareNPMVersionImage]: https://img.shields.io/npm/v/less-middleware.svg?style=flat +[less-middlewareNPMVersionImage]: https://badgen.net/npm/v/less-middleware?cache=86400 [lessGHUrl]: https://github.com/less/less.js -[lessDOCUrl]: http://lesscss.org/ +[lessDOCUrl]: https://lesscss.org/ [markedGHUrl]: https://github.com/markedjs/marked [markedDOCUrl]: https://github.com/markedjs/marked/blob/master/README.md [markedNPMUrl]: https://www.npmjs.com/package/marked -[markedNPMVersionImage]: https://img.shields.io/npm/v/marked.svg?style=flat +[markedNPMVersionImage]: https://badgen.net/npm/v/marked?cache=86400 + +[marked-highlightGHUrl]: https://github.com/markedjs/marked-highlight +[marked-highlightDOCUrl]: https://github.com/markedjs/marked-highlight/blob/main/README.md +[marked-highlightNPMUrl]: https://www.npmjs.com/package/marked-highlight +[marked-highlightNPMVersionImage]: https://badgen.net/npm/v/marked-highlight?cache=86400 [media-typeGHUrl]: https://github.com/lovell/media-type [media-typeDOCUrl]: https://github.com/lovell/media-type/blob/master/README.md [media-typeNPMUrl]: https://www.npmjs.com/package/media-type -[media-typeNPMVersionImage]: https://img.shields.io/npm/v/media-type.svg?style=flat +[media-typeNPMVersionImage]: https://badgen.net/npm/v/media-type?cache=86400 [method-overrideGHUrl]: https://github.com/expressjs/method-override [method-overrideDOCUrl]: https://github.com/expressjs/method-override/blob/master/README.md [method-overrideNPMUrl]: https://www.npmjs.com/package/method-override -[method-overrideNPMVersionImage]: https://img.shields.io/npm/v/method-override.svg?style=flat +[method-overrideNPMVersionImage]: https://badgen.net/npm/v/method-override?cache=86400 [mime-dbGHUrl]: https://github.com/jshttp/mime-db [mime-dbDOCUrl]: https://github.com/jshttp/mime-db/blob/master/README.md [mime-dbNPMUrl]: https://www.npmjs.com/package/mime-db -[mime-dbNPMVersionImage]: https://img.shields.io/npm/v/mime-db.svg?style=flat +[mime-dbNPMVersionImage]: https://badgen.net/npm/v/mime-db?cache=86400 [momentGHUrl]: https://github.com/moment/moment -[momentDOCUrl]: http://momentjs.com/docs/ +[momentDOCUrl]: https://momentjs.com/docs/ [momentNPMUrl]: https://www.npmjs.com/package/moment -[momentNPMVersionImage]: https://img.shields.io/npm/v/moment.svg?style=flat +[momentNPMVersionImage]: https://badgen.net/npm/v/moment?cache=86400 [moment-duration-formatGHUrl]: https://github.com/jsmreese/moment-duration-format [moment-duration-formatDOCUrl]: https://github.com/jsmreese/moment-duration-format/blob/master/README.md [moment-duration-formatNPMUrl]: https://www.npmjs.com/package/moment-duration-format -[moment-duration-formatNPMVersionImage]: https://img.shields.io/npm/v/moment-duration-format.svg?style=flat +[moment-duration-formatNPMVersionImage]: https://badgen.net/npm/v/moment-duration-format?cache=86400 [mongodbGHUrl]: https://github.com/mongodb/node-mongodb-native [mongodbDOCUrl]: https://github.com/mongodb/node-mongodb-native/blob/3.0/README.md [mongodbNPMUrl]: https://www.npmjs.com/package/mongodb -[mongodbNPMVersionImage]: https://img.shields.io/npm/v/mongodb.svg?style=flat +[mongodbNPMVersionImage]: https://badgen.net/npm/v/mongodb?cache=86400 -[mongooseGHUrl]: https://github.com/LearnBoost/mongoose -[mongooseDOCUrl]: http://mongoosejs.com +[mongooseGHUrl]: https://github.com/Automattic/mongoose +[mongooseDOCUrl]: https://mongoosejs.com [mongooseNPMUrl]: https://www.npmjs.com/package/mongoose -[mongooseNPMVersionImage]: https://img.shields.io/npm/v/mongoose.svg?style=flat +[mongooseNPMVersionImage]: https://badgen.net/npm/v/mongoose?cache=86400 [morganGHUrl]: https://github.com/expressjs/morgan [morganDOCUrl]: https://github.com/expressjs/morgan/blob/master/README.md [morganNPMUrl]: https://www.npmjs.com/package/morgan -[morganNPMVersionImage]: https://img.shields.io/npm/v/morgan.svg?style=flat +[morganNPMVersionImage]: https://badgen.net/npm/v/morgan?cache=86400 [mu2GHUrl]: https://github.com/raycmorgan/Mu [mu2DOCUrl]: https://github.com/raycmorgan/Mu/blob/master/README.md [mu2NPMUrl]: https://www.npmjs.com/package/mu2 -[mu2NPMVersionImage]: https://img.shields.io/npm/v/mu2.svg?style=flat +[mu2NPMVersionImage]: https://badgen.net/npm/v/mu2?cache=86400 [octiconsUrl]: https://octicons.github.com/ [octiconsGHUrl]: https://github.com/primer/octicons [octiconsDOCUrl]: https://github.com/primer/octicons#install [octiconsNPMUrl]: https://www.npmjs.com/package/octicons -[octiconsNPMVersionImage]: https://img.shields.io/npm/v/octicons.svg?style=flat +[octiconsNPMVersionImage]: https://badgen.net/npm/v/octicons?cache=86400 [passportGHUrl]: https://github.com/jaredhanson/passport -[passportDOCUrl]: http://passportjs.org/ +[passportDOCUrl]: https://www.passportjs.org/ [passportNPMUrl]: https://www.npmjs.com/package/passport -[passportNPMVersionImage]: https://img.shields.io/npm/v/passport.svg?style=flat +[passportNPMVersionImage]: https://badgen.net/npm/v/passport?cache=86400 [passport-openid]: https://github.com/OpenUserJs/passport-openid/tree/OpenID2 -[passport-facebookGHUrl]: https://github.com/jaredhanson/passport-facebook -[passport-facebookDOCUrl]: https://github.com/jaredhanson/passport-facebook/blob/master/README.md -[passport-facebookNPMUrl]: https://www.npmjs.com/package/passport-facebook -[passport-facebookNPMVersionImage]: https://img.shields.io/npm/v/passport-facebook.svg?style=flat - [passport-githubGHUrl]: https://github.com/jaredhanson/passport-github [passport-githubDOCUrl]: https://github.com/jaredhanson/passport-github/blob/master/README.md [passport-githubNPMUrl]: https://www.npmjs.com/package/passport-github -[passport-githubNPMVersionImage]: https://img.shields.io/npm/v/passport-github.svg?style=flat +[passport-githubNPMVersionImage]: https://badgen.net/npm/v/passport-github?cache=86400 [passport-gitlab2GHUrl]: https://github.com/fh1ch/passport-gitlab2 [passport-gitlab2DOCUrl]: https://github.com/fh1ch/passport-gitlab2/blob/master/README.md [passport-gitlab2NPMUrl]: https://www.npmjs.com/package/passport-gitlab2 -[passport-gitlab2NPMVersionImage]: https://img.shields.io/npm/v/passport-gitlab2.svg?style=flat +[passport-gitlab2NPMVersionImage]: https://badgen.net/npm/v/passport-gitlab2?cache=86400 [passport-google-oauth2GHUrl]: https://github.com/jaredhanson/passport-google-oauth2 [passport-google-oauth2DOCUrl]: https://github.com/jaredhanson/passport-google-oauth2/blob/master/README.md [passport-google-oauth2NPMUrl]: https://www.npmjs.com/package/passport-google-oauth20 -[passport-google-oauth2NPMVersionImage]: https://img.shields.io/npm/v/passport-google-oauth20.svg?style=flat +[passport-google-oauth2NPMVersionImage]: https://badgen.net/npm/v/passport-google-oauth20?cache=86400 [passport-imgurGHUrl]: https://github.com/mindfreakthemon/passport-imgur [passport-imgurDOCUrl]: https://github.com/mindfreakthemon/passport-imgur/blob/master/README.md [passport-imgurNPMUrl]: https://www.npmjs.com/package/passport-imgur -[passport-imgurNPMVersionImage]: https://img.shields.io/npm/v/passport-imgur.svg?style=flat +[passport-imgurNPMVersionImage]: https://badgen.net/npm/v/passport-imgur?cache=86400 [passport-redditGHUrl]: https://github.com/Slotos/passport-reddit [passport-redditDOCUrl]: https://github.com/Slotos/passport-reddit/blob/master/README.md [passport-redditNPMUrl]: https://www.npmjs.com/package/passport-reddit -[passport-redditNPMVersionImage]: https://img.shields.io/npm/v/passport-reddit.svg?style=flat +[passport-redditNPMVersionImage]: https://badgen.net/npm/v/passport-reddit?cache=86400 + +[passport-reddit-commonjsGHUrl]: https://github.com/536b656c6c79/passport-reddit-commonJS +[passport-reddit-commonjsDOCUrl]: https://github.com/536b656c6c79/passport-reddit-commonJS/blob/master/README.md +[passport-reddit-commonjsNPMUrl]: https://www.npmjs.com/package/passport-reddit-commonjs +[passport-reddit-commonjsNPMVersionImage]: https://badgen.net/npm/v/passport-reddit-commonjs?cache=86400 [passport-steamGHUrl]: https://github.com/liamcurry/passport-steam [passport-steamGHOpenIDUrl]: https://github.com/OpenUserJs/passport-steam/tree/OpenID2 [passport-steamDOCUrl]: https://github.com/liamcurry/passport-steam/blob/master/README.md [passport-steamNPMUrl]: https://www.npmjs.com/package/passport-steam -[passport-steamNPMVersionImage]: https://img.shields.io/npm/v/passport-steam.svg?style=flat - -[passport-twitterGHUrl]: https://github.com/jaredhanson/passport-twitter -[passport-twitterDOCUrl]: https://github.com/jaredhanson/passport-twitter/blob/master/README.md -[passport-twitterNPMUrl]: https://www.npmjs.com/package/passport-twitter -[passport-twitterNPMVersionImage]: https://img.shields.io/npm/v/passport-twitter.svg?style=flat - -[passport-yahooGHUrl]: https://github.com/jaredhanson/passport-yahoo -[passport-yahooGHOpenIDUrl]: https://github.com/OpenUserJs/passport-yahoo/tree/OpenID2 -[passport-yahooDOCUrl]: https://github.com/jaredhanson/passport-yahoo/blob/master/README.md -[passport-yahooNPMUrl]: https://www.npmjs.com/package/passport-yahoo -[passport-yahooNPMVersionImage]: https://img.shields.io/npm/v/passport-yahoo.svg?style=flat +[passport-steamNPMVersionImage]: https://badgen.net/npm/v/passport-steam?cache=86400 [pegjsGHUrl]: https://github.com/pegjs/pegjs [pegjsDOCUrl]: https://github.com/pegjs/pegjs/blob/master/README.md [pegjsNPMUrl]: https://www.npmjs.com/package/pegjs -[pegjsNPMVersionImage]: https://img.shields.io/npm/v/pegjs.svg?style=flat +[pegjsNPMVersionImage]: https://badgen.net/npm/v/pegjs?cache=86400 [rate-limit-mongoGHUrl]: https://github.com/2do2go/rate-limit-mongo [rate-limit-mongoDOCUrl]: https://github.com/2do2go/rate-limit-mongo/blob/master/README.md [rate-limit-mongoNPMUrl]: https://www.npmjs.com/package/rate-limit-mongo -[rate-limit-mongoNPMVersionImage]: https://img.shields.io/npm/v/rate-limit-mongo.svg?style=flat +[rate-limit-mongoNPMVersionImage]: https://badgen.net/npm/v/rate-limit-mongo?cache=86400 [remarkGHUrl]: https://github.com/remarkjs/remark [remarkDOCUrl]: https://github.com/remarkjs/remark/blob/master/readme.md [remarkNPMUrl]: https://www.npmjs.com/package/remark -[remarkNPMVersionImage]: https://img.shields.io/npm/v/remark.svg?style=flat +[remarkNPMVersionImage]: https://badgen.net/npm/v/remark?cache=86400 [remark-strip-htmlGHUrl]: https://github.com/craftzdog/remark-strip-html [remark-strip-htmlDOCUrl]: https://github.com/craftzdog/remark-strip-html/blob/master/readme.md [remark-strip-htmlNPMUrl]: https://www.npmjs.com/package/remark-strip-html -[remark-strip-htmlNPMVersionImage]: https://img.shields.io/npm/v/remark-strip-html.svg?style=flat +[remark-strip-htmlNPMVersionImage]: https://badgen.net/npm/v/remark-strip-html?cache=86400 [requestGHUrl]: https://github.com/request/request [requestDOCUrl]: https://github.com/request/request/blob/master/README.md [requestNPMUrl]: https://www.npmjs.com/package/request -[requestNPMVersionImage]: https://img.shields.io/npm/v/request.svg?style=flat +[requestNPMVersionImage]: https://badgen.net/npm/v/request?cache=86400 [rfc2047GHUrl]: https://github.com/One-com/rfc2047 [rfc2047DOCUrl]: https://github.com/One-com/rfc2047/blob/master/README.md [rfc2047NPMUrl]: https://www.npmjs.com/package/rfc2047 -[rfc2047NPMVersionImage]: https://img.shields.io/npm/v/rfc2047.svg?style=flat +[rfc2047NPMVersionImage]: https://badgen.net/npm/v/rfc2047?cache=86400 [s3rverGHUrl]: https://github.com/jamhall/s3rver [s3rverDOCUrl]: https://github.com/jamhall/s3rver/blob/master/README.md [s3rverNPMUrl]: https://www.npmjs.com/package/s3rver -[s3rverNPMVersionImage]: https://img.shields.io/npm/v/s3rver.svg?style=flat +[s3rverNPMVersionImage]: https://badgen.net/npm/v/s3rver?cache=86400 -[sanitize-htmlGHUrl]: https://github.com/punkave/sanitize-html -[sanitize-htmlDOCUrl]: https://github.com/punkave/sanitize-html/blob/master/README.md +[sanitize-htmlGHUrl]: https://github.com/apostrophecms/sanitize-html +[sanitize-htmlDOCUrl]: https://github.com/apostrophecms/sanitize-html/blob/master/README.md [sanitize-htmlNPMUrl]: https://www.npmjs.com/package/sanitize-html -[sanitize-htmlNPMVersionImage]: https://img.shields.io/npm/v/sanitize-html.svg?style=flat +[sanitize-htmlNPMVersionImage]: https://badgen.net/npm/v/sanitize-html?cache=86400 [select2GHUrl]: https://github.com/ivaynberg/select2 [select2DOCUrl]: https://select2.github.io/ [select2NPMUrl]: https://www.npmjs.com/package/select2 -[select2NPMVersionImage]: https://img.shields.io/npm/v/select2.svg?style=flat +[select2NPMVersionImage]: https://badgen.net/npm/v/select2?cache=86400 [select2-bootstrap-cssGHUrl]: https://github.com/t0m/select2-bootstrap-css/blob/bootstrap3/select2-bootstrap.css [select2-bootstrap-cssDOCUrl]: https://github.com/t0m/select2-bootstrap-css/blob/bootstrap3/README.md [select2-bootstrap-cssNPMUrl]: https://www.npmjs.com/package/select2-bootstrap-css -[select2-bootstrap-cssNPMVersionImage]: https://img.shields.io/npm/v/select2-bootstrap-css.svg?style=flat +[select2-bootstrap-cssNPMVersionImage]: https://badgen.net/npm/v/select2-bootstrap-css?cache=86400 [select2-bootstrap-cssGHHASHUrl]: https://github.com/t0m/select2-bootstrap-css/blob/fce5f9f984b0cc6c8483ce7225ad2639f3a4dae5/select2-bootstrap.css [serve-faviconGHUrl]: https://github.com/expressjs/serve-favicon [serve-faviconDOCUrl]: https://github.com/expressjs/serve-favicon/blob/master/README.md [serve-faviconNPMUrl]: https://www.npmjs.com/package/serve-favicon -[serve-faviconNPMVersionImage]: https://img.shields.io/npm/v/serve-favicon.svg?style=flat +[serve-faviconNPMVersionImage]: https://badgen.net/npm/v/serve-favicon?cache=86400 -[spdx-license-idsGHUrl]: https://github.com/shinnn/spdx-license-ids -[spdx-license-idsDOCUrl]: https://github.com/shinnn/spdx-license-ids/blob/master/README.md +[spdx-license-idsGHUrl]: https://github.com/jslicense/spdx-license-ids +[spdx-license-idsDOCUrl]: https://github.com/jslicense/spdx-license-ids/blob/master/README.md [spdx-license-idsNPMUrl]: https://www.npmjs.com/package/spdx-license-ids -[spdx-license-idsNPMVersionImage]: https://img.shields.io/npm/v/spdx-license-ids.svg?style=flat +[spdx-license-idsNPMVersionImage]: https://badgen.net/npm/v/spdx-license-ids?cache=86400 [strip-markdownGHUrl]: https://github.com/remarkjs/strip-markdown [strip-markdownDOCUrl]: https://github.com/remarkjs/strip-markdown/blob/master/readme.md [strip-markdownNPMUrl]: https://www.npmjs.com/package/strip-markdown -[strip-markdownNPMVersionImage]: https://img.shields.io/npm/v/strip-markdown.svg?style=flat +[strip-markdownNPMVersionImage]: https://badgen.net/npm/v/strip-markdown?cache=86400 [terserGHUrl]: https://github.com/terser/terser [terserDOCUrl]: https://github.com/terser/terser/blob/master/README.md [terserNPMUrl]: https://www.npmjs.com/package/terser -[terserNPMVersionImage]: https://img.shields.io/npm/v/terser.svg?style=flat +[terserNPMVersionImage]: https://badgen.net/npm/v/terser?cache=86400 [toobusy-jsGHUrl]: https://github.com/STRML/node-toobusy [toobusy-jsGHUrlHarmonyUrl]: https://github.com/OpenUserJs/node-toobusy/tree/harmony [toobusy-jsDOCUrl]: https://github.com/STRML/node-toobusy/blob/master/README.md [toobusy-jsNPMUrl]: https://npmjs.com/package/toobusy-js -[toobusy-jsNPMVersionImage]: https://img.shields.io/npm/v/toobusy-js.svg?style=flat +[toobusy-jsNPMVersionImage]: https://badgen.net/npm/v/toobusy-js?cache=86400 [underscoreGHUrl]: https://github.com/jashkenas/underscore -[underscoreDOCUrl]: http://underscorejs.org/ +[underscoreDOCUrl]: https://underscorejs.org/ +[underscoreDOCCLUrl]: https://underscorejs.org/#changelog [underscoreNPMUrl]: https://www.npmjs.com/package/underscore -[underscoreNPMVersionImage]: https://img.shields.io/npm/v/underscore.svg?style=flat +[underscoreNPMVersionImage]: https://badgen.net/npm/v/underscore?cache=86400 [useragentGHUrl]: https://github.com/3rd-Eden/useragent [useragentDOCUrl]: https://github.com/3rd-Eden/useragent/blob/master/README.md [useragentNPMUrl]: https://www.npmjs.com/package/useragent -[useragentNPMVersionImage]: https://img.shields.io/npm/v/useragent.svg?style=flat +[useragentNPMVersionImage]: https://badgen.net/npm/v/useragent?cache=86400 [bootswatchGHUrl]: https://github.com/thomaspark/bootswatch/blob/gh-pages/custom/bootstrap.css -[bootswatchREPOUrl]: http://bootswatch.com +[bootswatchREPOUrl]: https://bootswatch.com [bootswatchNPMUrl]: https://www.npmjs.com/package/bootswatch -[bootswatchNPMVersionImage]: https://img.shields.io/npm/v/bootswatch.svg?style=flat +[bootswatchNPMVersionImage]: https://badgen.net/npm/v/bootswatch?cache=86400 [bootswatchDOCUrl]: https://github.com/thomaspark/bootswatch/blob/gh-pages/README.md -[bootswatchBSUrl]: http://bootswatch.com/bower_components/bootstrap/dist/css/bootstrap.css [normalizeGHUrl]: https://github.com/necolas/normalize.css -[normalizeDOCUrl]: http://git.io/normalize +[normalizeDOCUrl]: https://github.com/necolas/normalize.css/blob/master/README.md [squadaOneGHUrl]: https://github.com/google/fonts/tree/master/ofl/squadaone [squadaOneREPOUrl]: https://www.google.com/fonts/specimen/Squada+One [squadaOneDOCUrl]: https://github.com/google/fonts/blob/master/README.md [squadaOneGHUrlRecent]: https://github.com/google/fonts/blob/master/ofl/squadaone/SquadaOne-Regular.ttf +[auth-oauth-appGHUrl]: https://github.com/octokit/auth-oauth-app.js +[auth-oauth-appDOCUrl]: https://github.com/octokit/auth-oauth-app.js/blob/master/README.md +[auth-oauth-appNPMUrl]: https://www.npmjs.com/package/@octokit/auth-oauth-app +[auth-oauth-appNPMVersionImage]: https://badgen.net/npm/v/@octokit/auth-oauth-app?cache=86400 + +[githubGHUrl]: https://github.com/octokit/rest.js +[githubDOCUrl]: https://github.com/octokit/rest.js/blob/master/README.md +[githubNPMUrl]: https://www.npmjs.com/package/@octokit/rest +[githubNPMVersionImage]: https://badgen.net/npm/v/@octokit/rest?cache=86400 [styleguide]: STYLEGUIDE.md [contributing]: .github/CONTRIBUTING.md -[gaCFGUrl]: https://www.google.com/analytics/ -[gaDOCUrl]: https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced -[gaCDNUrl]: //www.google-analytics.com/analytics.js - [oauthLogo]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/oauth.png "OAuth" [oauth1Logo]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/oauth1.png "OAuth1" [oauth2Logo]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/oauth2.png "OAuth2" diff --git a/app.js b/app.js index ad74394b1..7dcb11691 100755 --- a/app.js +++ b/app.js @@ -13,15 +13,23 @@ var isPro = require('./libs/debug').isPro; var isDev = require('./libs/debug').isDev; var isDbg = require('./libs/debug').isDbg; +var uaOUJS = require('./libs/debug').uaOUJS; + var isSecured = require('./libs/debug').isSecured; var privkey = require('./libs/debug').privkey; var fullchain = require('./libs/debug').fullchain; var chain = require('./libs/debug').chain; +var isRenewable = require('./libs/debug').isRenewable; // var path = require('path'); var crypto = require('crypto'); +var events = require('events'); +events.EventEmitter.defaultMaxListeners = 15; + +var ensureIntegerOrNull = require('./libs/helpers').ensureIntegerOrNull; + var express = require('express'); var toobusy = require('toobusy-js'); var statusCodePage = require('./libs/templateHelpers').statusCodePage; @@ -38,7 +46,7 @@ var Terser = require('terser'); var lessMiddleware = require('less-middleware'); var session = require('express-session'); -var MongoStore = require('connect-mongo')(session); +var MongoStore = require('connect-mongo'); var mongoose = require('mongoose'); mongoose.Promise = global.Promise; @@ -52,6 +60,7 @@ var pingCertTimer = null; var ttlSanityTimer = null; var app = express(); +app.disable('x-powered-by'); var modifySessions = require('./libs/modifySessions'); @@ -66,7 +75,7 @@ var _ = require('underscore'); var findSessionData = require('./libs/modifySessions').findSessionData; var dbOptions = {}; -var defaultPoolSize = 10; +var defaultPoolSize = ensureIntegerOrNull(process.env.CONNECT_POOL_SIZE) || 100; // Current *mongoose* default if (isPro) { dbOptions = { poolSize: defaultPoolSize, @@ -90,6 +99,7 @@ if (isPro) { } var fs = require('fs'); +var execSync = require('child_process').execSync; var http = require('http'); var https = require('https'); var sslOptions = null; @@ -100,11 +110,20 @@ app.set('port', process.env.PORT || 8080); app.set('securePort', process.env.SECURE_PORT || null); // Connect to the database -mongoose.connect(connectStr, dbOptions); +var clientP = mongoose.connect(connectStr, dbOptions).then( + function (aMongoose) { + return aMongoose.connection.getClient(); + } +); // Trap a few events for MongoDB -db.on('error', function () { - console.error(colors.red('MongoDB connection error')); +db.on('error', function (aErr) { + console.error( colors.red( [ + 'MongoDB connection error', + aErr.message, + 'Terminating app' + ].join('\n'))); + process.exit(1); // NOTE: Watchpoint }); db.once('open', function () { @@ -171,14 +190,13 @@ process.on('SIGINT', function () { process.exit(0); }); -var sessionStore = new MongoStore({ - mongooseConnection: db, +var sessionStore = MongoStore.create({ + clientPromise: clientP, autoRemove: 'native', ttl: settings.ttl.timerSanity * 60 // sec to min; 14 * 24 * 60 * 60 = 14 days. Default }); // See https://hacks.mozilla.org/2013/01/building-a-node-js-server-that-wont-melt-a-node-js-holiday-season-part-5/ -var ensureIntegerOrNull = require('./libs/helpers').ensureIntegerOrNull; var isSameOrigin = require('./libs/helpers').isSameOrigin; var maxLag = ensureIntegerOrNull(process.env.BUSY_MAXLAG) || 70; @@ -198,6 +216,8 @@ var maxMem = ensureIntegerOrNull(process.env.BUSY_MAXMEM) || 50; // 50% default var forceBusyAbsolute = process.env.FORCE_BUSY_ABSOLUTE === 'true'; var forceBusy = process.env.FORCE_BUSY === 'true'; +var forceBusyMessage = process.env.FORCE_BUSY_MESSAGE + || 'We are experiencing technical difficulties right now.'; app.use(function (aReq, aRes, aNext) { var pathname = aReq._parsedUrl.pathname; @@ -210,12 +230,37 @@ app.use(function (aReq, aRes, aNext) { aRes.oujsOptions = {}; } - // Middleware for DNT - aRes.oujsOptions.DNT = aReq.get('DNT') === '1' || aReq.get('DNT') === 'yes' ? true : false; - // Middleware for GDPR Notice aRes.oujsOptions.hideReminderGDPR = isSameOrigin(referer).result; + // Middleware for Notices + if (aReq._parsedUrl && aReq._parsedUrl.query) { + // NOTE: Keep in sync with muExpress.js, auth.js, user.js, and headerReminders.html + if (/^\/login$/.test(pathname)) { + // Middleware for Auth Notices + aRes.oujsOptions.showInvalidAuth = aReq.query.invalidauth === ''; + aRes.oujsOptions.showStratFail = aReq.query.stratfail === ''; + aRes.oujsOptions.showNoConsent = aReq.query.noconsent === ''; + aRes.oujsOptions.showNoName = aReq.query.noname === ''; + aRes.oujsOptions.showTooLong = aReq.query.toolong === ''; + aRes.oujsOptions.showUsernameFail = aReq.query.usernamefail === ''; + aRes.oujsOptions.showROAuth = aReq.query.roauth === ''; + aRes.oujsOptions.showRetryAuth = aReq.query.retryauth === ''; + aRes.oujsOptions.showAuthFail = aReq.query.authfail === ''; + } + + // NOTE: Keep in sync with muExpress, user.js and headerReminders.html + if (/^(?:\/admin\/session\/active|\/user\/preferences)$/.test(pathname)) { + // Middleware for Session Notices + aRes.oujsOptions.showSesssionNoExtend = aReq.query.noextend === ''; + aRes.oujsOptions.showSessionMissingUsername = aReq.query.noname === ''; + aRes.oujsOptions.showSesssionCurrentSessionProhibited = aReq.query.curses === ''; + aRes.oujsOptions.showSesssionHigherRankProhibited = aReq.query.hirank === ''; + aRes.oujsOptions.showSesssionNoOwned = aReq.query.noown === ''; + aRes.oujsOptions.showSesssionNoAdmin = aReq.query.noadmin === ''; + } + } + // if ( /^\/favicon\.ico$/.test(pathname) || @@ -244,7 +289,7 @@ app.use(function (aReq, aRes, aNext) { statusCodePage(aReq, aRes, aNext, { statusCode: 503, statusMessage: - 'We are experiencing technical difficulties right now. Please try again later.' + forceBusyMessage + ' Please try again later.' }); return; @@ -259,7 +304,7 @@ app.use(function (aReq, aRes, aNext) { if (usedMem > maxMem) { statusCodePage(aReq, aRes, aNext, { statusCode: 503, - statusMessage: 'We are very busy right now\u2026 Please try again later.' + statusMessage: 'We are very busy right now \u2026 Please try again later.' }); return; } @@ -273,7 +318,7 @@ app.use(function (aReq, aRes, aNext) { return; } else { aNext(); // not toobusy - // fallthrough + // fallsthrough } } }); @@ -331,27 +376,30 @@ if (isSecured) { secureServer.listen(app.get('securePort')); } catch (aE) { - console.error(colors.red('Server is NOT secured. Certificates may already be expired')); + console.error(colors.red('Server is NOT secured. Certificate may already be expired')); isSecured = false; - console.warn(colors.cyan('Attempting to rename certificates')); + console.warn(colors.cyan('Attempting to rename errored certificate')); try { - fs.renameSync(privkey, privkey + '.expired') - fs.renameSync(fullchain, fullchain + '.expired'); - fs.renameSync(chain, chain + '.expired'); + fs.renameSync(privkey, privkey + '.error') + fs.renameSync(fullchain, fullchain + '.error'); + fs.renameSync(chain, chain + '.error'); - console.warn(colors.green('Certificates renamed')); + console.warn(colors.green('Errored certificate renamed')); // NOTE: Cached modules and/or callbacks may not reflect this change immediately // so must conclude with server trip } catch (aE) { - console.warn(colors.red('Error renaming certificates')); + console.warn(colors.red('Error renaming errored certificate')); } - // Trip the server now to try any alternate fallback certificates - // If there aren't any it should run in http mode however usually no access through web + // Trip the server now to try any alternate fallback certificate or updated + // If there aren't any it should run in http mode however usually no easy access through web // This should prevent logging DoS + // NOTE: Pro will always stay in unsecure for `setInterval` value plus + // ~ n seconds (hours) until renewable check if available. + beforeExit(); // NOTE: Event not triggered for direct `process.exit()` process.exit(1); @@ -364,7 +412,15 @@ server.listen(app.get('port')); if (isDev || isDbg) { app.use(morgan('dev')); } else if (process.env.FORCE_MORGAN_PREDEF_FORMAT) { - app.use(morgan(process.env.FORCE_MORGAN_PREDEF_FORMAT)); + app.use(morgan(process.env.FORCE_MORGAN_PREDEF_FORMAT, { + skip: function (aReq, aRes) { + if (process.env.FORCE_MORGAN_PREDEF_FORMAT_SKIP === 'true') { + return (aRes.statusCode >= 400 && aRes.statusCode <= 499 || aRes.statusCode === 304); + } else { + return false; + } + } + })); } app.use(bodyParser.urlencoded({ @@ -391,16 +447,16 @@ app.use(session({ saveUninitialized: false, unset: 'destroy', cookie: { - maxAge: 5 * 60 * 1000, // minutes in ms NOTE: Expanded after successful auth + maxAge: 2 * 60 * 1000, // minutes in ms NOTE: Expanded after successful auth secure: (isSecured ? true : false), - sameSite: 'lax' // NOTE: Current auth necessity + sameSite: 'strict' }, rolling: true, secret: sessionSecret, store: sessionStore })); app.use(function (aReq, aRes, aNext) { - if (aReq.session[passport._key]) { + if (aReq.session && aReq.session[passport._key]) { // load data from existing session aReq._passport.session = aReq.session[passport._key]; } @@ -453,7 +509,8 @@ app.use(function(aReq, aRes, aNext) { /^\/mod\/removed\//.test(pathname) ) { aRes.minifyOptions = aRes.minifyOptions || {}; // Ensure object exists on response - aRes.minifyOptions.minify = false; // Skip using release minification because we control this with *terser* + aRes.minifyOptions.minify = false; // Skip minification because we use *terser* with .js + aRes.minifyOptions.enabled = false; // Force no processing on remainder of types on these routes } aNext(); }); @@ -472,39 +529,56 @@ require('./routes')(app); // Timers -function tripServerOnCertExpire(aValidToString) { +function tripServerOnCertExpire(aValidToString, aStayResident) { var tlsDate = new Date(aValidToString); - var nowDate = new Date(); + var now = new Date(); + var trippy = false; var tripDate = new Date(tlsDate.getTime() - (2 * 60 * 60 * 1000)); // ~2 hours before fault - if (nowDate.getTime() >= tripDate.getTime()) { - console.error(colors.red('Certificates expiring very soon. Tripping server to unsecure mode')); + console.log('Validating certificate'); - isSecured = false; + if (now.getTime() >= tripDate.getTime()) { + console.error(colors.red('Valid secure certificate not available.')); - console.warn(colors.cyan('Attempting to rename certificates')); - try { - fs.renameSync(privkey, privkey + '.expiring') - fs.renameSync(fullchain, fullchain + '.expiring'); - fs.renameSync(chain, chain + '.expiring'); - console.log(colors.green('Certificates renamed')); + if (!isRenewable && isSecured) { + console.warn(colors.cyan('Attempting to rename expiring certificate')); + try { + fs.renameSync(privkey, privkey + '.expiry'); + fs.renameSync(fullchain, fullchain + '.expiry'); + fs.renameSync(chain, chain + '.expiry'); - // NOTE: Cached modules and/or callbacks may not reflect this change immediately - // so must conclude with server trip + console.log(colors.green('Expiring certificate renamed')); + trippy = true; - } catch (aE) { - console.warn(colors.red('Error renaming certificates')); + // NOTE: Cached modules and/or callbacks may not reflect this change immediately + // so must conclude with server trip + + } catch (aE) { + console.error(colors.red('Error renaming expiring certificate ' + aE.code)); + } + } else if (isSecured) { + console.warn(colors.cyan('Attempting to renew expiring certificate')); + + try { + execSync(process.env.ATTEMPT_RENEWAL); // NOTE: Synchronous wait + trippy = true; + + } catch (aE) { + console.warn(colors.red('Error renewing expiring certificate ' + aE.code)); + } } - // Trip the server now to try any alternate fallback certificates - // If there aren't any it should run in http mode however usually no access through web - // This should prevent logging DoS + if (trippy && !aStayResident) { + // Trip the server now to try any alternate fallback certificate or updated + // If there aren't any it should run in http mode however usually no easy access through web + // This should prevent logging DoS - beforeExit(); // NOTE: Event not triggered for direct `process.exit()` + beforeExit(); // NOTE: Event not triggered for direct `process.exit()` - process.exit(1); + process.exit(1); + } } } @@ -516,7 +590,10 @@ function pingCert() { (isPro && app.get('securePort') ? ':' + app.get('securePort') : ':' + app.get('port')) - + '/api' + + '/api', + headers: { + 'User-Agent': uaOUJS + (process.env.UA_SECRET ? ' ' + process.env.UA_SECRET : '') + } }, function (aErr, aRes, aBody) { if (aErr) { if (aErr.cert) { @@ -525,15 +602,22 @@ function pingCert() { // browsers as well as false credentials supplied // Test for time limit of expiration - tripServerOnCertExpire(aErr.cert.valid_to); + tripServerOnCertExpire(new Date(aErr.cert.valid_to).toUTCString()); // NOTE: Strong coercion } else { console.warn([ - colors.red(aErr), - colors.red('Server may not be running on specified port or port blocked by firewall'), - colors.red('Encryption not available') + colors.red(aErr.code), + colors.red('Server may not be running on specified port or port blocked by firewall.'), + colors.red('Encryption may not be available.') ].join('\n')); + + if (isDev && aErr.code === 'EPROTO') { + tripServerOnCertExpire(new Date().toUTCString(), true); // NOTE: Strong coercion + } else { + tripServerOnCertExpire(new Date().toUTCString()); // NOTE: Strong coercion + } + } return; } @@ -544,23 +628,26 @@ function pingCert() { console.warn(colors.red('Firewall pass-through detected')); // Test for time limit of expiration - tripServerOnCertExpire(aRes.req.connection.getPeerCertificate().valid_to); + tripServerOnCertExpire( + new Date(aRes.req.connection.getPeerCertificate().valid_to).toUTCString()); // NOTE: Strong coercion } else { - console.warn(colors.yellow('Encryption not available')); + console.warn(colors.yellow('Encryption not available.')); } }); }; -if (isSecured) { - pingCertTimer = setInterval(pingCert, 60 * 60 * 1000); // NOTE: Check every hour -} +// Re-test cert at init +pingCert(); + +// Re-test cert at interval +pingCertTimer = setInterval(pingCert, (isPro ? 60 * 60 : 1 * 15) * 1000); // NOTE: Check about every hour for pro function ttlSanity() { var options = {}; findSessionData({}, sessionStore, options, function (aErr) { if (aErr) { - console.error('some error during ttlSanity', aErr); + console.error('Some error during ttlSanity', aErr); return; } diff --git a/controllers/admin.js b/controllers/admin.js index e060c2246..cbad96492 100644 --- a/controllers/admin.js +++ b/controllers/admin.js @@ -34,6 +34,7 @@ var Vote = require('../models/vote').Vote; var modelParser = require('../libs/modelParser'); var nil = require('../libs/helpers').nil; +var baseOrigin = require('../libs/helpers').baseOrigin; var loadPassport = require('../libs/passportLoader').loadPassport; var strategyInstances = require('../libs/passportLoader').strategyInstances; @@ -377,6 +378,8 @@ exports.adminSessionActiveView = function (aReq, aRes, aNext) { var store = aReq.sessionStore; + var thisURL = null; + // Session options.authedUser = authedUser = modelParser.parseUser(authedUser); options.isMod = authedUser && authedUser.isMod; @@ -396,6 +399,15 @@ exports.adminSessionActiveView = function (aReq, aRes, aNext) { username = aReq.query.q; + // redirectTo (forced) + thisURL = new URL(aReq.url, baseOrigin); + ['noname', 'curses', 'hirank', 'noown', 'noadmin', 'noextend'] + .forEach(function (aE, aI, aA) { + thisURL.searchParams.delete(aE); + } + ); + options.redirectToo = thisURL.pathname + (thisURL.search ? thisURL.search : ''); + // Page metadata pageMetadata(options, ['Sessions', 'Admin']); diff --git a/controllers/auth.js b/controllers/auth.js index 90e407a01..6ad16070b 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -8,8 +8,8 @@ var isDbg = require('../libs/debug').isDbg; // //--- Dependency inclusions +var moment = require('moment'); var passport = require('passport'); -var url = require('url'); var colors = require('ansi-colors'); //--- Model inclusions @@ -25,12 +25,17 @@ var loadPassport = require('../libs/passportLoader').loadPassport; var strategyInstances = require('../libs/passportLoader').strategyInstances; var verifyPassport = require('../libs/passportVerify').verify; var cleanFilename = require('../libs/helpers').cleanFilename; +var getRedirect = require('../libs/helpers').getRedirect; +var isSameOrigin = require('../libs/helpers').isSameOrigin; var addSession = require('../libs/modifySessions').add; var expandSession = require('../libs/modifySessions').expand; var statusCodePage = require('../libs/templateHelpers').statusCodePage; +var modelParser = require('../libs/modelParser'); + //--- Configuration inclusions var allStrategies = require('./strategies.json'); +var settings = require('../models/settings.json'); //--- @@ -42,7 +47,13 @@ passport.serializeUser(function (aUser, aDone) { // Setup all the auth strategies var openIdStrategies = {}; Strategy.find({}, function (aErr, aStrategies) { - // WARNING: No err handling + if (aErr) { + // Some possible catastrophic error + console.error(colors.red(aErr)); + + process.exit(1); + return; + } // Get OpenId strategies for (var name in allStrategies) { @@ -58,20 +69,101 @@ Strategy.find({}, function (aErr, aStrategies) { }); }); -// Get the referer url for redirect after login/logout -// WARNING: Also found in `./controller/index.js` -function getRedirect(aReq) { - var referer = aReq.get('Referer'); - var redirect = '/'; +exports.preauth = function (aReq, aRes, aNext) { + var authedUser = aReq.session.user; + + var username = aReq.body.username; + var userauth = aReq.body.auth; + var SITEKEY = process.env.HCAPTCHA_SITE_KEY; + + if (!authedUser) { + if (!username) { + aRes.redirect('/login?noname'); + return; + } + // Clean the username of leading and trailing whitespace, + // and other stuff that is unsafe in a url + username = cleanFilename(username.replace(/^\s+|\s+$/g, '')); - if (referer) { - referer = url.parse(referer); - if (referer.hostname === aReq.hostname) { - redirect = referer.path; + // The username could be empty after the replacements + if (!username) { + aRes.redirect('/login?noname'); + return; } + + if (username.length > 64) { + aRes.redirect('/login?toolong'); + return; + } + + User.findOne({ name: { $regex: new RegExp('^' + username + '$', 'i') } }, + function (aErr, aUser) { + var user = null; + + if (aErr) { + console.error('Authfail with no User found of', username, aErr); + aRes.redirect('/login?usernamefail'); + return; + } + + if (aUser) { + user = modelParser.parseUser(aUser); + + // Ensure that casing is identical so we still have it, correctly, when they + // get back from authentication + aReq.body.username = user.name; + + if (userauth) { + aReq.body.userauth = userauth; + } else { + aReq.body.userauth = user.userStrategies[user.userStrategies.length - 1]; + } + aReq.userrole = user.roleName; + + if (!user._probationary) { + // Skip captcha for well known individual + aReq.wellKnownUser = true; + + exports.auth(aReq, aRes, aNext); + } else { + // Validate captcha for lesser known individual + if (!SITEKEY) { + // Skip captcha for not implemented + exports.auth(aReq, aRes, aNext); + } else { + aNext(); + } + } + } else { + // Match cleansed name and this is the casing they have chosen + aReq.body.username = username; + + aReq.body.userauth = userauth; + aReq.newUser = true; + + // Validate captcha for unknown individual + if (!SITEKEY) { + // Skip captcha for not implemented + exports.auth(aReq, aRes, aNext); + } else { + aNext(); + } + } + }); + } else { + // Skip captcha for already logged in + exports.auth(aReq, aRes, aNext); } - return redirect; +}; + +exports.errauth = function (aErr, aReq, aRes, aNext) { + if (aErr) { + console.error(aErr.status, aErr.message); + aRes.redirect(302, '/login?authfail'); + } else { + aNext(); + } } exports.auth = function (aReq, aRes, aNext) { @@ -95,7 +187,16 @@ exports.auth = function (aReq, aRes, aNext) { if (aReq.session.cookie.sameSite !== 'lax') { aReq.session.cookie.sameSite = 'lax'; aReq.session.save(function (aErr) { - // WARNING: No err handling + if (aErr) { + // Some possible catastrophic error + console.error(colors.red(aErr)); + + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: 'Save Session failed.' + }); + return; + } authenticate(aReq, aRes, aNext); }); @@ -104,108 +205,151 @@ exports.auth = function (aReq, aRes, aNext) { } } + function sessionauth() { + var redirectTo = null; + var captchaToken = aReq.body['g-captcha-response'] ?? aReq.body['h-captcha-response']; + + // Yet another passport hack. + // Initialize the passport session data only when we need it. i.e. late binding + if (!aReq.session[passportKey] && aReq._passport.session) { + aReq.session[passportKey] = {}; + aReq._passport.session = aReq.session[passportKey]; + } + + // Validate and save redirect url from the form submission on the session + redirectTo = isSameOrigin(aReq.body.redirectTo || getRedirect(aReq)); + if (redirectTo.result) { + aReq.session.redirectTo = redirectTo.URL.pathname; + } else { + delete aReq.body.redirectTo; + aReq.session.redirectTo = '/'; + } + + // Save the known statuses of the user on the session and remove + aReq.session.userauth = aReq.body.userauth; + aReq.session.userrole = aReq.userrole; + aReq.session.wellKnownUser = aReq.wellKnownUser; + aReq.session.newUser = aReq.newUser; + delete aReq.userrole; + delete aReq.wellKnownUser; + delete aReq.newUser; + + // Save the token from the captcha on the session and remove from body + if (captchaToken) { + aReq.session.captchaToken = captchaToken; + aReq.session.captchaSuccess = aReq.hcaptcha; + + delete aReq.body['g-captcha-response']; + delete aReq.body['h-captcha-response']; + delete aReq.hcaptcha; + } + } + + function anteauth() { + // Store the useragent always so we still have it when they + // get back from authentication and/or attaching + aReq.session.useragent = aReq.get('user-agent'); + + User.findOne({ name: username }, + function (aErr, aUser) { + var strategies = null; + var strat = null; + + if (aErr) { // NOTE: Possible DB error + console.error('Authfail with no User found of', username, aErr); + aRes.redirect('/login?usernamefail'); + return; + } + + if (aUser) { + strategies = aUser.strategies; + strat = strategies.pop(); + + if (aReq.session.newstrategy) { // authenticate with a new strategy + strategy = aReq.session.newstrategy; + } else if (!strategy) { // use an existing strategy + strategy = strat; + } else if (strategies.indexOf(strategy) === -1) { + // add a new strategy but first authenticate with existing strategy + aReq.session.newstrategy = strategy; + strategy = strat; + } // else { + // use the strategy that was given in the POST + // } + } + + if (!strategy) { + aRes.redirect('/login?stratfail'); + return; + } else { + auth(); + return; + } + } + ); + } + var authedUser = aReq.session.user; var consent = aReq.body.consent; var strategy = aReq.body.auth || aReq.params.strategy; - var username = aReq.body.username || aReq.session.username || - (authedUser ? authedUser.name : null); + var username = null; var authOpts = { failureRedirect: '/login?stratfail' }; var passportKey = aReq._passport.instance._key; - // Yet another passport hack. - // Initialize the passport session data only when we need it. - if (!aReq.session[passportKey] && aReq._passport.session) { - aReq.session[passportKey] = {}; - aReq._passport.session = aReq.session[passportKey]; - } + if (!authedUser) { + // Already validated username + username = aReq.body.username; - // Save redirect url from the form submission on the session - aReq.session.redirectTo = aReq.body.redirectTo || getRedirect(aReq); - - // Allow a logged in user to add a new strategy - if (strategy && authedUser) { - aReq.session.passport.oujsOptions.authAttach = true; - aReq.session.newstrategy = strategy; - aReq.session.username = authedUser.name; - } else if (authedUser) { - aRes.redirect(aReq.session.redirectTo || '/'); - delete aReq.session.redirectTo; - return; - } else if (consent !== 'true') { - aRes.redirect('/login?noconsent'); - return; - } + if (consent !== 'true') { + aRes.redirect('/login?noconsent'); + return; + } - if (!username) { - aRes.redirect('/login?noname'); - return; - } - // Clean the username of leading and trailing whitespace, - // and other stuff that is unsafe in a url - username = cleanFilename(username.replace(/^\s+|\s+$/g, '')); + sessionauth(); - // The username could be empty after the replacements - if (!username) { - aRes.redirect('/login?noname'); - return; - } - - // Store the username in the session so we still have it when they - // get back from authentication - if (!aReq.session.username) { + // Store the username always so we still have it when they + // get back from authentication aReq.session.username = username; - } - // Store the useragent always so we still have it when they - // get back from authentication and attaching - aReq.session.useragent = aReq.get('user-agent'); - User.findOne({ name: { $regex: new RegExp('^' + username + '$', 'i') } }, - function (aErr, aUser) { - var strategies = null; - var strat = null; + anteauth(); - if (aErr) { - console.error('Authfail with no User found of', username, aErr); - aRes.redirect('/login?usernamefail'); - return; - } + } else { + // Already validated username + username = aReq.session.username || (authedUser ? authedUser.name : null); - if (aUser) { - // Ensure that casing is identical so we still have it, correctly, when they - // get back from authentication - if (aUser.name !== username) { - aReq.session.username = aUser.name; - } - strategies = aUser.strategies; - strat = strategies.pop(); - - if (aReq.session.newstrategy) { // authenticate with a new strategy - strategy = aReq.session.newstrategy; - } else if (!strategy) { // use an existing strategy - strategy = strat; - } else if (strategies.indexOf(strategy) === -1) { - // add a new strategy but first authenticate with existing strategy - aReq.session.newstrategy = strategy; - strategy = strat; - } // else use the strategy that was given in the POST - } + sessionauth(); - if (!strategy) { - aRes.redirect('/login'); - return; - } else { - auth(); - return; - } - }); + // Allow a logged in user to add a new strategy + if (strategy) { + aReq.session.passport.oujsOptions.authAttach = true; + aReq.session.newstrategy = strategy; + aReq.session.username = authedUser.name; + } else { + aRes.redirect(aReq.session.redirectTo || '/'); + delete aReq.session.redirectTo; + return; + } + + anteauth(); + } }; exports.callback = function (aReq, aRes, aNext) { var strategy = aReq.params.strategy; var username = aReq.session.username; + var wellKnownUser = aReq.session.wellKnownUser; var newstrategy = aReq.session.newstrategy; + var captchaToken = aReq.session.captchaToken; + var captchaSuccess = aReq.session.captchaSuccess; + var strategyInstance = null; var doneUri = aReq.session.user ? '/user/preferences' : '/'; + var SITEKEY = process.env.HCAPTCHA_SITE_KEY; + + if (SITEKEY && !wellKnownUser && !captchaToken && !captchaSuccess) { + aRes.redirect('/login?authfail'); + return; + } // The callback was called improperly or sesssion expired if (!strategy || !username) { @@ -280,6 +424,14 @@ exports.callback = function (aReq, aRes, aNext) { } aReq.logIn(aUser, function (aErr) { + var now = null; + var lastAuthed = null; + + var fudgeMin = settings.fudgeMin; + var fudgeSec = settings.fudgeSec; + + var waitAuthCapMin = isDev ? settings.waitAuthCapMin.dev: settings.waitAuthCapMin.pro; + if (aErr) { console.error('Not logged in'); console.error(aErr); @@ -292,8 +444,61 @@ exports.callback = function (aReq, aRes, aNext) { } // Show a console notice that successfully logged in - if (isDev || isDbg) { - console.log(colors.green('Logged in')); + now = new Date(); + + console.log( + colors.green('Logged in'), + aUser.name, + colors.green('at'), + aReq.connection.remoteAddress, + colors.green('on'), + now.toISOString() + ); + + lastAuthed = aUser.authed; + + // Save the last date a user sucessfully logged in + aUser.authed = now; + + // Check probationary status vs lastAuthed for alt IP circumvention prevention + if (aUser._probationary && lastAuthed && !newstrategy) { + if (!moment().isAfter(moment(lastAuthed).add(waitAuthCapMin, 'minutes'))) { + aUser.save(function (aErr, aUser) { + if (aErr) { + // NOTE: A user could get back in quicker but still delayed from `authed` + console.error( + colors.red( + 'Probationary logged out failed to write current authentication date to aUser'), + aUser.name, + colors.red('at'), + aReq.connection.remoteAddress, + colors.red('on'), + now.toISOString() + ); + } + }); + + console.log( + colors.red('Logged out probationary User'), + aUser.name, + colors.red('at'), + aReq.connection.remoteAddress, + colors.red('on'), + now.toISOString() + ); + + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitAuthCapMin * 60 + (isDev ? fudgeSec : fudgeMin) + } + }); + return; + } } // Store the user info in the session @@ -312,8 +517,6 @@ exports.callback = function (aReq, aRes, aNext) { aReq.session.passport.oujsOptions.strategy = strategy; } - // Save the last date a user sucessfully logged in - aUser.authed = new Date(); // Save consent aUser.consented = true; @@ -327,6 +530,26 @@ exports.callback = function (aReq, aRes, aNext) { } addSession(aReq, aUser, function () { + var ID = null; + var intervalId = function () { + if (aReq.session.cookie.sameSite !== 'strict') { + aReq.session.cookie.sameSite = 'strict'; + aReq.session.save(function (aErr, aSession) { + if (aErr) { + // Some catastrophic error + console.error(colors.red(aErr)); + return; + } + if (ID) { + clearTimeout(ID); + } + }) + }; + }; + var timeoutId = function () { + ID = setInterval(intervalId, 1); + } + if (newstrategy && newstrategy !== strategy) { // Allow a user to link to another account aRes.redirect('/auth/' + newstrategy); // NOTE: Watchpoint... careful with encoding @@ -340,7 +563,16 @@ exports.callback = function (aReq, aRes, aNext) { if (!aReq.session.passport.oujsOptions.authAttach) { expandSession(aReq, aUser, function (aErr) { - // WARNING: No err handling + if (aErr) { + // Some possible catastrophic error + console.error(colors.red(aErr)); + + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: 'Expand Session failed.' + }); + return; + } aRes.redirect(doneUri); }); @@ -350,12 +582,7 @@ exports.callback = function (aReq, aRes, aNext) { // Ensure `sameSite` is set to max after redirect // Elevate for optimal future protection - setTimeout(function () { - if (aReq.session.cookie.sameSite !== 'strict') { - aReq.session.cookie.sameSite = 'strict'; - aReq.session.save(); - } - }, 500); + setTimeout(timeoutId, 0); } }); }); diff --git a/controllers/discussion.js b/controllers/discussion.js index 2d34c38e8..76c9a875b 100644 --- a/controllers/discussion.js +++ b/controllers/discussion.js @@ -29,9 +29,9 @@ var modelQuery = require('../libs/modelQuery'); var cleanFilename = require('../libs/helpers').cleanFilename; var execQueryTask = require('../libs/tasks').execQueryTask; -var statusCodePage = require('../libs/templateHelpers').statusCodePage; var pageMetadata = require('../libs/templateHelpers').pageMetadata; var orderDir = require('../libs/templateHelpers').orderDir; +var statusCodePage = require('../libs/templateHelpers').statusCodePage; //--- Configuration inclusions @@ -41,23 +41,27 @@ var categories = [ { slug: 'announcements', name: 'Announcements', - description: 'UserScripts News (OpenUserJS, GreaseMonkey, etc)', + description: 'UserScripts News (OpenUserJS, Greasemonkey, etc)', + active: true, roleReqToPostTopic: 3 // Moderator }, { slug: 'garage', name: 'The Garage', - description: 'Talk shop, and get help with user script development' + description: 'Talk shop, and get help with user script development', + active: true }, { slug: 'corner', - name: 'Beggar\'s Corner', - description: 'Propose ideas and request user scripts' + name: 'Beggars Corner', + description: 'Propose ideas and request user scripts', + active: true }, { slug: 'discuss', name: 'General Discussion', - description: 'Off-topic discussion about anything related to user scripts or OpenUserJS.org' + description: 'Off-topic discussion about anything related to user scripts or OpenUserJS.org', + active: true }, { slug: 'issues', @@ -69,6 +73,7 @@ var categories = [ slug: 'all', name: 'All Discussions', description: 'Overview of all discussions', + clear: true, virtual: true } ]; @@ -139,6 +144,14 @@ exports.categoryListPage = function (aReq, aRes, aNext) { // discussionListQuery: Defaults modelQuery.applyDiscussionListQueryDefaults(discussionListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // discussionListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -230,6 +243,14 @@ exports.list = function (aReq, aRes, aNext) { // discussionListQuery: Defaults modelQuery.applyDiscussionListQueryDefaults(discussionListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // discussionListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -477,14 +498,15 @@ function postTopic(aUser, aCategory, aTopic, aContent, aIssue, aUserAgent, aCall Discussion.findOne({ path: path }, null, params, function (aErr, aDiscussion) { var newDiscussion = null; + var now = new Date(); var props = { topic: aTopic, category: aCategory, comments: 0, author: aUser.name, - created: new Date(), + created: now, lastCommentor: aUser.name, - updated: new Date(), + updated: now, rating: 0, flagged: false, path: path, @@ -534,6 +556,12 @@ exports.createTopic = function (aReq, aRes, aNext) { var content = aReq.body['comment-content']; var userAgent = aReq.headers['user-agent']; + var parser = 'UserScript'; + var rHeaderContent = new RegExp( + '^(?:\\uFEFF)?\/\/ ==' + parser + '==([\\s\\S]*?)^\/\/ ==\/'+ parser + '==', 'm' + ); + var headerContent = null; + if (!category) { aNext(); return; @@ -560,6 +588,16 @@ exports.createTopic = function (aReq, aRes, aNext) { return; } + // Simple validation check + headerContent = rHeaderContent.exec(content); + if (headerContent) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Source Code not allowed in Comment.' + }); + return; + } + postTopic(authedUser, category.slug, topic, content, false, userAgent, function (aDiscussion) { if (!aDiscussion) { exports.newTopic(aReq, aRes, aNext); @@ -585,6 +623,12 @@ exports.createComment = function (aReq, aRes, aNext) { var content = aReq.body['comment-content']; var userAgent = aReq.headers['user-agent']; + var parser = 'UserScript'; + var rHeaderContent = new RegExp( + '^(?:\\uFEFF)?\/\/ ==' + parser + '==([\\s\\S]*?)^\/\/ ==\/'+ parser + '==', 'm' + ); + var headerContent = null; + if (!aDiscussion) { aNext(); return; @@ -592,12 +636,22 @@ exports.createComment = function (aReq, aRes, aNext) { if (!content || !content.trim()) { statusCodePage(aReq, aRes, aNext, { - statusCode: 403, + statusCode: 403, // Forbidden statusMessage: 'You cannot post an empty comment to this discussion' }); return; } + // Simple validation check + headerContent = rHeaderContent.exec(content); + if (headerContent) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Source Code not allowed in Comment.' + }); + return; + } + postComment(authedUser, aDiscussion, content, false, userAgent, function (aDiscussion) { if (!aDiscussion) { statusCodePage(aReq, aRes, aNext, { diff --git a/controllers/document.js b/controllers/document.js index ee22f9b83..7a96e7d41 100644 --- a/controllers/document.js +++ b/controllers/document.js @@ -72,6 +72,12 @@ exports.view = function (aReq, aRes, aNext) { case 'QupZilla': aRes.redirect(301, aReq.url.replace(/QupZilla\/?$/, 'Falkon')); return; + case 'Violentmonkey-for-Chrome': + aRes.redirect(301, aReq.url.replace(/Violentmonkey-for-Chrome\/?$/, 'Violentmonkey-for-Brave')); + return; + case 'Violentmonkey-for-Chromium': + aRes.redirect(301, aReq.url.replace(/Violentmonkey-for-Chromium\/?$/, 'Violentmonkey-for-Brave')); + return; } documentPath = 'views/includes/documents'; @@ -128,16 +134,21 @@ exports.view = function (aReq, aRes, aNext) { return; } - // Dynamically create a file listing of the pages + // Dynamically create a TOC file listing of the pages options.fileList = []; - for (file in aFileList) { - if (/\.md$/.test(aFileList[file])) { - options.fileList.push({ - href: aFileList[file].replace(/\.md$/, ''), - textContent: aFileList[file].replace(/\.md$/, '').replace(/-/g, ' ') - }); + for (file in aFileList.sort( + function (aA, aB) { + return aA.localeCompare(aB, 'en', { 'sensitivity' : 'base' }); + })) { + + if (/\.md$/.test(aFileList[file])) { + options.fileList.push({ + active: document === aFileList[file].replace(/\.md$/, ''), + href: aFileList[file].replace(/\.md$/, ''), + textContent: aFileList[file].replace(/\.md$/, '').replace(/-/g, ' ') + }); + } } - } aCallback(null); }); diff --git a/controllers/flag.js b/controllers/flag.js index 241f5776f..708f58d25 100644 --- a/controllers/flag.js +++ b/controllers/flag.js @@ -26,6 +26,7 @@ var flagLib = require('../libs/flag'); var statusCodePage = require('../libs/templateHelpers').statusCodePage; //--- Configuration inclusions +var userRoles = require('../models/userRoles.json'); //--- @@ -46,7 +47,7 @@ exports.flag = function (aReq, aRes, aNext) { form.parse(aReq, function (aErr, aFields) { // WARNING: No err handling - var flag = aFields.flag === 'false' ? false : true; + var flag = aFields.flag && aFields.flag[0] ? aFields.flag[0] : null; var reason = null; var type = aReq.params[0]; @@ -57,8 +58,18 @@ exports.flag = function (aReq, aRes, aNext) { var authedUser = aReq.session.user; + if (!flag) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, + statusMessage: 'Missing flag field.' + }); + return; + } + + flag = aFields.flag[0] === 'false' ? false : true; + if (flag) { - reason = aFields.reason; + reason = aFields.reason && aFields.reason[0] ? aFields.reason[0] : null; // Check to make sure form submission has this name available. // This occurs either when no reason is supplied, @@ -66,20 +77,30 @@ exports.flag = function (aReq, aRes, aNext) { if (!reason) { statusCodePage(aReq, aRes, aNext, { statusCode: 403, - statusMessage: 'Missing reason for removal.' + statusMessage: 'Missing reason for flagging.' }); return; } // Simple error check for string null and limit to max characters reason = reason.trim(); - if (reason === '' || reason.length > 300) { + + if (reason === '') { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, + statusMessage: 'Invalid reason for flagging.' + }); + return; + } + + if (reason.length > 300) { statusCodePage(aReq, aRes, aNext, { statusCode: 403, - statusMessage: 'Invalid reason for removal.' + statusMessage: 'Reason for flagging too long.' }); return; } + } switch (type) { @@ -184,8 +205,9 @@ exports.getFlaggedListForContent = function (aModelName, aOptions, aCallback) { contentList[aContentKey].flaggedList.push({ name: aUser.name, + rank: userRoles[aUser.role], reason: aFlagList[aFlagKey].reason, - since: aFlagList[aFlagKey]._since + since: aFlagList[aFlagKey].created }); aEachInnerCallback(); }); diff --git a/controllers/group.js b/controllers/group.js index e1a6974db..d806dd2e9 100644 --- a/controllers/group.js +++ b/controllers/group.js @@ -28,6 +28,7 @@ var getRating = require('../libs/collectiveRating').getRating; var execQueryTask = require('../libs/tasks').execQueryTask; var pageMetadata = require('../libs/templateHelpers').pageMetadata; var orderDir = require('../libs/templateHelpers').orderDir; +var statusCodePage = require('../libs/templateHelpers').statusCodePage; //--- Configuration inclusions @@ -132,10 +133,12 @@ exports.addScriptToGroups = function (aScript, aGroupNames, aCallback) { // Create a custom group for the script if (!aScript._groupId && newGroup) { tasks.push(function (aCallback) { + var now = new Date(); var group = new Group({ name: newGroup, rating: 0, - updated: new Date(), + created: now, + updated: now, _scriptIds: [aScript._id] }); @@ -186,7 +189,7 @@ exports.addScriptToGroups = function (aScript, aGroupNames, aCallback) { }; // list groups -exports.list = function (aReq, aRes) { +exports.list = function (aReq, aRes, aNext) { function preRender() { // groupList options.groupList = _.map(options.groupList, modelParser.parseGroup); @@ -236,6 +239,14 @@ exports.list = function (aReq, aRes) { // groupListQuery: Defaults modelQuery.applyGroupListQueryDefaults(groupListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // groupListQuery: Pagination var pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -372,6 +383,14 @@ exports.view = function (aReq, aRes, aNext) { // scriptListQuery: Defaults modelQuery.applyScriptListQueryDefaults(scriptListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // scriptListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults diff --git a/controllers/index.js b/controllers/index.js index a18fd7414..897146585 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -11,6 +11,7 @@ var isDbg = require('../libs/debug').isDbg; var async = require('async'); var _ = require('underscore'); var url = require('url'); +var crypto = require('crypto'); //--- Model inclusions var Discussion = require('../models/discussion').Discussion; @@ -27,6 +28,8 @@ var getFlaggedListForContent = require('./flag').getFlaggedListForContent; //--- Library inclusions // var indexLib = require('../libs/index'); +var getRedirect = require('../libs/helpers').getRedirect; + var modelParser = require('../libs/modelParser'); var modelQuery = require('../libs/modelQuery'); @@ -42,7 +45,7 @@ var strategies = require('./strategies.json'); //--- // The site home page has scriptList, and groups in a sidebar -exports.home = function (aReq, aRes) { +exports.home = function (aReq, aRes, aNext) { function preRender() { // scriptList options.scriptList = _.map(options.scriptList, modelParser.parseScript); @@ -100,7 +103,7 @@ exports.home = function (aReq, aRes) { async.parallel([ function (aCallback) { - if (!!!options.isFlagged || !options.isAdmin) { // NOTE: Watchpoint + if (!!!options.isFlagged || !options.isMod) { // NOTE: Watchpoint aCallback(); return; } @@ -156,6 +159,14 @@ exports.home = function (aReq, aRes) { modelQuery.applyScriptListQueryDefaults(scriptListQuery, options, aReq); } + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // scriptListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -195,21 +206,6 @@ exports.home = function (aReq, aRes) { async.parallel(tasks, asyncComplete); }; -// Get the referer url for redirect after login/logout -function getRedirect(aReq) { - var referer = aReq.get('Referer'); - var redirect = '/'; - - if (referer) { - referer = url.parse(referer); - if (referer.hostname === aReq.hostname) { - redirect = referer.path; - } - } - - return redirect; -} - // UI for user registration exports.register = function (aReq, aRes) { // @@ -217,12 +213,16 @@ exports.register = function (aReq, aRes) { var authedUser = aReq.session.user; var tasks = []; + var SITEKEY = process.env.HCAPTCHA_SITE_KEY; + // If already logged in, go back. if (authedUser) { aRes.redirect(getRedirect(aReq)); return; } + options.hasCaptcha = (SITEKEY ? SITEKEY : ''); + options.redirectTo = getRedirect(aReq); // Page metadata @@ -246,7 +246,8 @@ exports.register = function (aReq, aRes) { if (!aStrategy.oauth) { options.strategies.push({ 'strat': aStrategyKey, - 'display': aStrategy.name + 'display': aStrategy.name, + 'disabled': aStrategy.readonly }); } }); @@ -254,6 +255,10 @@ exports.register = function (aReq, aRes) { // Strategy.find({}, function (aErr, aAvailableStrategies) { + var SITEKEY = process.env.HCAPTCHA_SITE_KEY; + var defaultCSP = ' https: \'self\''; + var captchaCSP = (SITEKEY ? ' hcaptcha.com *.hcaptcha.com' : ''); + if (aErr || !aAvailableStrategies) { statusCodePage(aReq, aRes, aNext, { statusCode: 503, @@ -265,10 +270,16 @@ exports.register = function (aReq, aRes) { aAvailableStrategies.forEach(function (aStrategy) { options.strategies.push({ 'strat': aStrategy.name, - 'display': aStrategy.display + 'display': aStrategy.display, + 'disabled': (strategies[aStrategy.name] ? strategies[aStrategy.name].readonly : true) }); }); + options.hasCaptcha = (SITEKEY ? SITEKEY : ''); + + options.nonce = crypto.randomBytes(512).toString('base64'); + defaultCSP += ' \'nonce-' + options.nonce + '\''; + // Insert an empty default strategy at the beginning // NOTE: Safari always autoselects an option when disabled options.strategies.unshift({'strat': '', 'display': '(default preferred authentication)'}); @@ -278,10 +289,27 @@ exports.register = function (aReq, aRes) { return aStrategy.display; }); + aRes.header('Cache-Control', 'no-cache, no-store, must-revalidate'); aRes.header('Pragma', 'no-cache'); aRes.header('Expires', '0'); + // + aRes.header('Content-Security-Policy', + 'default-src \'none\'' + + '; base-uri' + defaultCSP + + '; child-src' + defaultCSP + + '; connect-src' + defaultCSP + captchaCSP + + '; font-src' + defaultCSP + + '; frame-src' + defaultCSP + captchaCSP + + '; img-src' + defaultCSP + + '; script-src' + defaultCSP + captchaCSP + + '; style-src' + defaultCSP + captchaCSP + + '; form-action' + defaultCSP + + '; navigate-to' + defaultCSP + + '' + ); + aRes.render('pages/loginPage', options); } }); diff --git a/controllers/issue.js b/controllers/issue.js index 9507d9c20..10ed7e9d6 100644 --- a/controllers/issue.js +++ b/controllers/issue.js @@ -28,9 +28,9 @@ var modelQuery = require('../libs/modelQuery'); var execQueryTask = require('../libs/tasks').execQueryTask; var countTask = require('../libs/tasks').countTask; -var statusCodePage = require('../libs/templateHelpers').statusCodePage; var pageMetadata = require('../libs/templateHelpers').pageMetadata; var orderDir = require('../libs/templateHelpers').orderDir; +var statusCodePage = require('../libs/templateHelpers').statusCodePage; //--- Configuration inclusions @@ -159,11 +159,19 @@ exports.list = function (aReq, aRes, aNext) { // discussionListQuery: Defaults modelQuery.applyDiscussionListQueryDefaults(discussionListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // discussionListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults // SearchBar - options.searchBarPlaceholder = 'Search Issues'; + options.searchBarPlaceholder = options.searchBarPlaceholder.replace(/Topics/, 'Issues'); //--- Tasks @@ -273,8 +281,9 @@ exports.view = function (aReq, aRes, aNext) { options.discussion = discussion; options.canClose = authedUser && - (authedUser._id == script._authorId || authedUser._id == discussion._authorId); - options.canOpen = authedUser && authedUser._id == script._authorId; + (authedUser._id == script._authorId || authedUser._id == discussion._authorId) && + discussion.open; + options.canOpen = authedUser && (authedUser._id == script._authorId) && !discussion.open; // commentListQuery commentListQuery = Comment.find(); @@ -285,6 +294,14 @@ exports.view = function (aReq, aRes, aNext) { // commentListQuery: Defaults modelQuery.applyCommentListQueryDefaults(commentListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // commentListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -348,6 +365,12 @@ exports.open = function (aReq, aRes, aNext) { var userAgent = aReq.headers['user-agent']; var tasks = []; + var parser = 'UserScript'; + var rHeaderContent = new RegExp( + '^(?:\\uFEFF)?\/\/ ==' + parser + '==([\\s\\S]*?)^\/\/ ==\/'+ parser + '==', 'm' + ); + var headerContent = null; + // Session options.authedUser = authedUser = modelParser.parseUser(authedUser); options.isMod = authedUser && authedUser.isMod; @@ -377,6 +400,16 @@ exports.open = function (aReq, aRes, aNext) { return; } + // Simple validation check + headerContent = rHeaderContent.exec(content); + if (headerContent) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Source Code not allowed in Comment.' + }); + return; + } + // Issue Submission discussionLib.postTopic(authedUser, category.slug, topic, content, true, userAgent, function (aDiscussion) { @@ -419,6 +452,12 @@ exports.comment = function (aReq, aRes, aNext) { var category = type + '/' + installNameBase + '/issues'; var topic = aReq.params.topic; + var parser = 'UserScript'; + var rHeaderContent = new RegExp( + '^(?:\\uFEFF)?\/\/ ==' + parser + '==([\\s\\S]*?)^\/\/ ==\/'+ parser + '==', 'm' + ); + var headerContent = null; + if (aErr || !aScript) { aNext(); return; @@ -426,12 +465,22 @@ exports.comment = function (aReq, aRes, aNext) { if (!content || !content.trim()) { statusCodePage(aReq, aRes, aNext, { - statusCode: 403, + statusCode: 403, // Forbidden statusMessage: 'You cannot post an empty comment to this issue' }); return; } + // Simple validation check + headerContent = rHeaderContent.exec(content); + if (headerContent) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Source Code not allowed in Comment.' + }); + return; + } + discussionLib.findDiscussion(category, topic, function (aIssue) { // var authedUser = aReq.session.user; diff --git a/controllers/moderation.js b/controllers/moderation.js index b90f76c38..fdc55c587 100644 --- a/controllers/moderation.js +++ b/controllers/moderation.js @@ -42,10 +42,10 @@ exports.removedItemPage = function (aReq, aRes, aNext) { options.isMod = authedUser && authedUser.isMod; options.isAdmin = authedUser && authedUser.isAdmin; - if (!options.isMod) { + if (!options.isAdmin) { statusCodePage(aReq, aRes, aNext, { statusCode: 403, - statusMessage: 'This page is only accessible by moderators', + statusMessage: 'This page is only accessible by admins', }); return; } @@ -160,6 +160,13 @@ exports.removedItemListPage = function (aReq, aRes, aNext) { break; default: modelQuery.applyRemovedItemListQueryDefaults(removedItemListQuery, options, aReq); + options.filterUser = true; + options.filterScript = true; + options.filterComment = true; + options.filterDiscussion = true; + options.filterFlag = true; + options.filterGroup = true; + options.filterVote = true; } // removedItemListQuery: Pagination diff --git a/controllers/remove.js b/controllers/remove.js index db62f7f4d..9c5d252b0 100644 --- a/controllers/remove.js +++ b/controllers/remove.js @@ -44,7 +44,7 @@ exports.rm = function (aReq, aRes, aNext) { form.parse(aReq, function (aErr, aFields) { // WARNING: No err handling - var reason = aFields.reason; + var reason = aFields.reason && aFields.reason[0] ? aFields.reason[0] : null; var type = aReq.params[0]; var isLib = null; diff --git a/controllers/script.js b/controllers/script.js index 31f109200..62f330da4 100644 --- a/controllers/script.js +++ b/controllers/script.js @@ -29,6 +29,7 @@ var getFlaggedListForContent = require('./flag').getFlaggedListForContent; //--- Library inclusions // var scriptLib = require('../libs/script'); +var statusCodePage = require('../libs/templateHelpers').statusCodePage; var isSameOrigin = require('../libs/helpers').isSameOrigin; var voteLib = require('../libs/vote'); @@ -80,6 +81,8 @@ var getScriptPageTasks = function (aOptions) { var copyright = null; var license = null; var licenseConflict = false; + var antifeature = null; + var types = []; var author = null; var collaborator = null; @@ -174,6 +177,24 @@ var getScriptPageTasks = function (aOptions) { aOptions.script.licenseConflict = true; } + // Show antifeatures of the script + antifeature = scriptStorage.findMeta(script.meta, 'UserScript.antifeature'); + if (antifeature) { + aOptions.hasAntiFeature = true; + + antifeature.forEach(function (aElement, aIndex, aArray) { + var type = types[aElement.value1]; + var comment = type ? (type.comment || '') : ''; + + types[aElement.value1] = { name: aElement.value1, comment: + (aElement.value2 ? aElement.value2 : '') + + (comment ? (aElement.value2 ? '\n' : '') + comment: '') + }; + }); + + aOptions.script.antifeatures = Object.values(types).reverse(); + } + // Show collaborators of the script author = scriptStorage.findMeta(script.meta, 'OpenUserJS.author.0.value'); collaborator = scriptStorage.findMeta(script.meta, 'OpenUserJS.collaborator'); @@ -340,7 +361,7 @@ exports.view = function (aReq, aRes, aNext) { async.parallel([ function (aCallback) { - if (!options.isAdmin) { // NOTE: Watchpoint + if (!options.isMod) { // NOTE: Watchpoint aCallback(); return; } @@ -438,6 +459,12 @@ exports.edit = function (aReq, aRes, aNext) { var scriptGroups = null; var tasks = []; + var parser = 'UserScript'; + var rHeaderContent = new RegExp( + '^(?:\\uFEFF)?\/\/ ==' + parser + '==([\\s\\S]*?)^\/\/ ==\/'+ parser + '==', 'm' + ); + var headerContent = null; + // --- if (aErr || !aScript) { aNext(); @@ -474,6 +501,16 @@ exports.edit = function (aReq, aRes, aNext) { // POST aScript.about = aReq.body.about; + // Simple validation check + headerContent = rHeaderContent.exec(aScript.about); + if (headerContent) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Source Code not allowed in Script Info.' + }); + return; + } + remark().use(stripHTML).use(stripMD).process(aScript.about, function(aErr, aFile) { if (aErr || !aFile) { aScript._about = ( diff --git a/controllers/scriptStorage.js b/controllers/scriptStorage.js index 4fdb25ffe..99a38ceb8 100644 --- a/controllers/scriptStorage.js +++ b/controllers/scriptStorage.js @@ -4,8 +4,12 @@ var isPro = require('../libs/debug').isPro; var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; -var isSecured = require('../libs/debug').isSecured; var statusError = require('../libs/debug').statusError; +var isSecured = require('../libs/debug').isSecured; +var uaOUJS = require('../libs/debug').uaOUJS; + +var rLogographic = require('../libs/debug').rLogographic; +var logographicDivisor = require('../libs/debug').logographicDivisor; // @@ -26,7 +30,6 @@ var mediaType = require('media-type'); var mediaDB = require('mime-db'); var async = require('async'); var moment = require('moment'); -var Base62 = require('base62/lib/ascii'); var SPDX = require('spdx-license-ids'); var sizeOf = require('image-size'); var ipRangeCheck = require("ip-range-check"); @@ -40,7 +43,7 @@ var Discussion = require('../models/discussion').Discussion; //--- Controller inclusions //--- Library inclusions -// var scriptStorageLib = require('../libs/scriptStorage'); +var scriptStorageLib = require('../libs/scriptStorage'); var patternHasSameOrigin = require('../libs/helpers').patternHasSameOrigin; var patternMaybeSameOrigin = require('../libs/helpers').patternMaybeSameOrigin; @@ -193,7 +196,7 @@ if (isSecured) { request({ url: 'https://api.github.com/meta', headers: { - 'User-Agent': 'OpenUserJS' + 'User-Agent': uaOUJS + (process.env.UA_SECRET ? ' ' + process.env.UA_SECRET : '') } }, function (aErr, aRes, aBody) { var meta = null; @@ -214,7 +217,7 @@ if (isSecured) { try { meta = JSON.parse(aBody); } catch (aE) { - console.error(colors.red('Error retrieving GitHub `hooks`', aE)); + console.error(colors.red('Error retrieving GitHub `hooks` ' + aE)); return; } @@ -224,7 +227,8 @@ if (isSecured) { githubHookAddresses.push(aEl); } else { console.warn( - colors.yellow('GitHub `hooks` element', aEl, 'does not match IPv4 CIDR specification') + colors.yellow('GitHub `hooks` element ' + aEl + + ' does not match IPv4 CIDR specification') ); } }); @@ -547,28 +551,19 @@ exports.sendScript = function (aReq, aRes, aNext) { ((aReq.headers.accept || '*/*').split(',').indexOf('text/x-userscript-meta') > -1 || rMetaJS.test(pathname))) { + // Search engine affirmation + aRes.set('X-Robots-Tag', 'noindex'); + exports.sendMeta(aReq, aRes, aNext); return; } exports.getSource(aReq, function (aScript, aStream) { let chunks = []; - let updateURL = null; - let updateUtf = null; - - let matches = null; - let rAnyLocalMetaUrl = new RegExp( - '^' + patternHasSameOrigin + - '/(?:meta|install|src/scripts)/(.+?)/(.+?)\.(?:meta|user)\.js$' - ); - let hasAlternateLocalUpdateURL = false; - - let rSameOrigin = new RegExp( - '^' + patternHasSameOrigin - ); var lastModified = null; var eTag = null; + var hashSRI = null; var maxAge = 7 * 60 * 60 * 24; // nth day(s) in seconds var now = null; var continuation = true; @@ -578,59 +573,35 @@ exports.sendScript = function (aReq, aRes, aNext) { return; } - if (process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true') { - // `@updateURL` must be exact here for OUJS hosted checks - // e.g. no `search`, no `hash` - - updateURL = findMeta(aScript.meta, 'UserScript.updateURL.0.value'); - if (updateURL) { - - // Check for decoding error - try { - updateUtf = decodeURIComponent(updateURL); - } catch (aE) { - aRes.set('Warning', '199 ' + aReq.headers.host + - rfc2047.encode(' Invalid @updateURL')); - aRes.status(400).send(); // Bad request - return; - } - - // Validate `author` and `name` (installNameBase) to this scripts meta only - let matches = updateUtf.match(rAnyLocalMetaUrl); - if (matches) { - if (cleanFilename(aScript.author, '').toLowerCase() + - '/' + cleanFilename(aScript.name, '') === matches[1].toLowerCase() + '/' + matches[2]) - { - // Same script - } else { - hasAlternateLocalUpdateURL = true; - } - } else { - // Allow offsite checks - updateURL = new URL(updateURL); - if (rSameOrigin.test(updateURL.origin)) { - hasAlternateLocalUpdateURL = true; - } - } - } else { - if (!aScript.isLib) { - // Don't serve the script anywhere in this mode and if absent - hasAlternateLocalUpdateURL = true; - } - } - - if (hasAlternateLocalUpdateURL) { - aRes.set('Warning', '199 ' + aReq.headers.host + - rfc2047.encode(' Invalid @updateURL in lockdown')); - aRes.status(404).send(); // Not found - return; - } + if (scriptStorageLib.invalidKey( + aScript.author, + aScript.name, + aScript.isLib, + 'updateURL', + findMeta(aScript.meta, 'UserScript.updateURL.0.value') + )) { + aRes.set('Warning', '199 ' + aReq.headers.host + + rfc2047.encode(' Invalid @updateURL') + + (process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true' ? ' in lockdown.' : '.')); + + // NOTE: Force HTTP stack response for Chromium based browsers #1856 + aRes.set('Content-Type', 'text/html; charset=UTF-8'); + + aRes.status(403).send(); // Forbidden + return; } + hashSRI = aScript.hash + ? 'sha512-' + Buffer.from(aScript.hash, 'hex').toString('base64') + : 'undefined'; + // HTTP/1.1 Caching aRes.set('Cache-Control', 'public, max-age=' + maxAge + ', no-cache, no-transform, must-revalidate'); + // Search engine affirmation + aRes.set('X-Robots-Tag', 'noindex'); + // Only minify for response that doesn't contain `.min.` extension if (!/\.min(\.user)?\.js$/.test(aReq._parsedUrl.pathname) || process.env.DISABLE_SCRIPT_MINIFICATION === 'true') { @@ -638,8 +609,8 @@ exports.sendScript = function (aReq, aRes, aNext) { lastModified = moment(aScript.updated) .utc().format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT'; - // Convert a based representation of the hex sha512sum - eTag = '"' + Base62.encode(parseInt('0x' + aScript.hash, 16)) + ' .user.js"'; + // Use SRI of the stored sha512sum + eTag = '"' + hashSRI + ' .user.js"'; // If already client-side... HTTP/1.1 Caching if (aReq.get('if-none-match') === eTag || aReq.get('if-modified-since') === lastModified) { @@ -682,6 +653,9 @@ exports.sendScript = function (aReq, aRes, aNext) { source = chunks.join(''); // NOTE: Watchpoint // Send the script + if (aScript.isLib) { + aRes.set('Access-Control-Allow-Origin', '*'); + } aRes.set('Content-Type', 'text/javascript; charset=UTF-8'); aStream.setEncoding('utf8'); @@ -693,6 +667,8 @@ exports.sendScript = function (aReq, aRes, aNext) { aRes.set('Last-modified', lastModified); aRes.set('Etag', eTag); + aRes.set('Content-Length', Buffer.byteLength(source, 'utf8')); + aRes.write(source); aRes.end(); @@ -705,6 +681,16 @@ exports.sendScript = function (aReq, aRes, aNext) { return; } + // Don't count installs from tagged XHR + if (aReq.get('x-requested-with')) { + return; + } + + // Don't count installs on browser request in Fx + if (aReq.get('accept') && aReq.get('accept').indexOf('text/html') > -1) { // NOTE: Watchpoint + return; + } + // Update the install count ++aScript.installs; ++aScript.installsSinceUpdate; @@ -785,10 +771,12 @@ exports.sendScript = function (aReq, aRes, aNext) { } else { source = result.code; - // Calculate a based representation of the hex sha512sum - eTag = '"' + Base62.encode( - parseInt('0x' + crypto.createHash('sha512').update(source).digest('hex'), 16)) + - ' .min.user.js"'; + // Calculate SRI of the source sha512sum + eTag = '"sha512-' + + Buffer.from( + crypto.createHash('sha512').update(source).digest('base64') + ) + ' .min.user.js"'; + } } catch (aE) { // On any failure default to unminified if (isDev) { @@ -816,8 +804,8 @@ exports.sendScript = function (aReq, aRes, aNext) { lastModified = moment(aScript.updated) .utc().format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT'; - // Reset to convert a based representation of the hex sha512sum - eTag = '"' + Base62.encode(parseInt('0x' + aScript.hash, 16)) + ' .user.js"'; + // Reset SRI of the stored sha512sum + eTag = '"' + hashSRI + ' .user.js"'; } // If already client-side... partial HTTP/1.1 Caching @@ -844,6 +832,8 @@ exports.sendScript = function (aReq, aRes, aNext) { aRes.set('Last-Modified', lastModified); aRes.set('Etag', eTag); + aRes.set('Content-Length', Buffer.byteLength(source, 'utf8')); + aRes.write(source); aRes.end(); @@ -876,7 +866,34 @@ exports.sendMeta = function (aReq, aRes, aNext) { } function render() { - aRes.end(JSON.stringify(meta, null, isPro ? '' : ' ')); + var metaJSON = JSON.stringify(meta, null, isPro ? '' : ' '); + var hash = null; + + // HTTP/1.1 Caching + aRes.set('Cache-Control', 'public, max-age=' + maxAge + + ', no-cache, no-transform, must-revalidate'); + + // Use SRI of the recalculated sha512sum + hash = crypto.createHash('sha512').update(metaJSON).digest('hex'); + eTag = '"' + 'sha512-' + Buffer.from(hash, 'hex').toString('base64') + ' .meta.json"'; + + // If already client-side... HTTP/1.1 Caching + if (aReq.get('if-none-match') === eTag) { + aRes.status(304).send(); // Not Modified + return; + } + + // Okay to send .meta.json... + aRes.set('Content-Type', 'application/json; charset=UTF-8'); + + // HTTP/1.0 Caching + aRes.set('Expires', moment(moment() + maxAge * 1000).utc() + .format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT'); + + // HTTP/1.1 Caching + aRes.set('Etag', eTag); + + aRes.end(metaJSON); } function asyncComplete(aErr) { @@ -892,6 +909,9 @@ exports.sendMeta = function (aReq, aRes, aNext) { var installNameBase = getInstallNameBase(aReq, { hasExtension: true }); var meta = null; + var eTag = null; + var maxAge = 7 * 60 * 60 * 24; // nth day(s) in seconds + Script.findOne({ installName: caseSensitive(installNameBase + '.user.js') }, function (aErr, aScript) { // WARNING: No err handling @@ -901,40 +921,16 @@ exports.sendMeta = function (aReq, aRes, aNext) { var whitespace = '\u0020\u0020\u0020\u0020'; var tasks = []; - var eTag = null; - var maxAge = 7 * 60 * 60 * 24; // nth day(s) in seconds - if (!aScript) { aNext(); return; } - // HTTP/1.1 Caching - aRes.set('Cache-Control', 'public, max-age=' + maxAge + - ', no-cache, no-transform, must-revalidate'); - script = modelParser.parseScript(aScript); meta = script.meta; // NOTE: Watchpoint if (/\.json$/.test(aReq.params.scriptname)) { - // Create a based representation of the hex sha512sum - eTag = '"' + Base62.encode(parseInt('0x' + aScript.hash, 16)) + ' .meta.json"'; - - // If already client-side... HTTP/1.1 Caching - if (aReq.get('if-none-match') === eTag) { - aRes.status(304).send(); // Not Modified - return; - } - - // Okay to send .meta.json... - aRes.set('Content-Type', 'application/json; charset=UTF-8'); - - // HTTP/1.0 Caching - aRes.set('Expires', moment(moment() + maxAge * 1000).utc() - .format('ddd, DD MMM YYYY HH:mm:ss') + ' GMT'); - - // HTTP/1.1 Caching - aRes.set('Etag', eTag); + // NOTE: Caching is managed on rendering for more live stats // Check for existance of OUJS metadata block if (!meta.OpenUserJS) { @@ -944,7 +940,8 @@ exports.sendMeta = function (aReq, aRes, aNext) { // Overwrite any keys found with the following... meta.OpenUserJS.installs = [{ value: script.installs }]; meta.OpenUserJS.issues = [{ value: 'n/a' }]; - meta.OpenUserJS.hash = aScript.hash ? [{ value: aScript.hash }] : undefined; + meta.OpenUserJS.rating = [{ value: script.rating }]; + meta.OpenUserJS.hash = script.hash ? [{ value: script.hashSRI }] : 'undefined'; // Get the number of open issues scriptOpenIssueCountQuery = Discussion.find({ category: exports @@ -955,8 +952,8 @@ exports.sendMeta = function (aReq, aRes, aNext) { async.parallel(tasks, asyncComplete); } else { - // Create a based representation of the hex sha512sum - eTag = '"' + Base62.encode(parseInt('0x' + aScript.hash, 16)) + ' .meta.js"'; + // Use SRI of the stored sha512sum + eTag = '"' + script.hashSRI + ' .meta.js"'; // If already client-side... HTTP/1.1 Caching if (aReq.get('if-none-match') === eTag) { @@ -1027,7 +1024,9 @@ exports.findMeta = findMeta; // Parse a specific metadata block content with a specified *pegjs* parser function parseMeta(aParser, aString) { var lines = {}; + var CEVLines = {}; var rLine = /\/\/ @(\S+)(?:\s+(.*))?/; + var rCEVLine = /\/\/@/; var line = null; var header = null; var key = null; @@ -1042,6 +1041,10 @@ function parseMeta(aParser, aString) { return (aElement.match(rLine)); }); + CEVLines = aString.split(/[\r\n]+/).filter(function (aElement, aIndex, aArray) { + return (aElement.match(rCEVLine)); + }); + for (line in lines) { try { header = aParser.parse(lines[line], { startRule: 'line' }); @@ -1091,6 +1094,15 @@ function parseMeta(aParser, aString) { }); } + headers[':CVE'] = []; // NOTE: Important to prevent spoofing + + if (CEVLines.length > 0) { + headers[':CVE'].push( + { + value: 'Comment only in Metadata Block may be parsed as a Key in certain non-standard UserScript engines.' + } + ); + } return headers; } exports.parseMeta = parseMeta; @@ -1109,6 +1121,8 @@ exports.getMeta = function (aBufs, aCallback) { var hasUserScriptHeaderContent = false; var blocksContent = {}; var blocks = {}; + var CVE = null; + var CVES = []; for (; i < aBufs.length; ++i) { // Accumulate the indexed Buffer length to use with `totalLength` parameter @@ -1136,8 +1150,22 @@ exports.getMeta = function (aBufs, aCallback) { for (parser in parsers) { if (blocksContent[parser]) { blocks[parser] = parseMeta(parsers[parser], blocksContent[parser]); + if (parser !== 'OpenUserJS' && blocks[parser][':CVE']) { + for (CVE in blocks[parser][':CVE']) { + CVES.push(blocks[parser][':CVE'][CVE]); + } + } + delete blocks[parser][':CVE'] } } + + if (CVES.length > 0) { + if (!blocks['OpenUserJS']) { + blocks['OpenUserJS'] = {}; + } + blocks['OpenUserJS'].CVE = CVES; + } + aCallback(blocks); return; } @@ -1170,10 +1198,12 @@ function isEqualKeyset(aSlaveKeyset, aMasterKeyset) { exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { var isLib = !!findMeta(aMeta, 'UserLibrary'); + var userName = findMeta(aMeta, 'OpenUserJS.author.0.value') || aUser.name; var scriptName = null; var scriptDescription = null; var thisName = null; var thisDescription = null; + var hasInvalidKey = null; async.series([ function (aInnerCallback) { @@ -1519,14 +1549,19 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { req = request.get({ url: icon, headers: { - 'User-Agent': 'request' + 'User-Agent': 'request' // NOTE: Anonymous intended } }) .on('response', function (aRes) { // TODO: Probably going to be something here }) .on('error', function (aErr) { - aInnerCallback(aErr); + if (aErr && aErr.code === 'ECONNRESET') { + console.error('*request* ECONNRESET error with `@icon` validation at', icon); + // fallsthrough + } else { + aInnerCallback(aErr); + } }) .on('data', function (aChunk) { var buf = null; @@ -1636,20 +1671,17 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { aInnerCallback(null); }, function (aInnerCallback) { - // `@updateURL` validations - var updateURL = null; - - updateURL = findMeta(aMeta, 'UserScript.updateURL.0.value'); - if (updateURL) { - if (!isFQUrl(updateURL)) { - - // Not a web url... reject - aInnerCallback(new statusError({ - message: '`@updateURL` not a web url', - code: 400 - }), null); - return; - } + // `@updateURL` validation + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'updateURL', + findMeta(aMeta, 'UserScript.updateURL.0.value')); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; } aInnerCallback(null); @@ -1701,28 +1733,189 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { aInnerCallback(null); }, + function (aInnerCallback) { + // `@include` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'include', + findMeta(aMeta, 'UserScript.include.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@match` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'match', + findMeta(aMeta, 'UserScript.match.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, function (aInnerCallback) { // `@exclude` validations - var excludes = null; - var missingExcludeAll = true; - - if (isLib) { - excludes = findMeta(aMeta, 'UserScript.exclude.value'); - if (excludes) { - excludes.forEach(function (aElement, aIndex, aArray) { - if (aElement === '*') { - missingExcludeAll = false; - } - }); - } + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'exclude', + findMeta(aMeta, 'UserScript.exclude.value') + ); - if (missingExcludeAll) { - aInnerCallback(new statusError({ - message: 'UserScript Metadata Block missing `@exclude *`.', - code: 400 - }), null); - return; - } + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@grant` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'grant', + findMeta(aMeta, 'UserScript.grant.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@require` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'require', + findMeta(aMeta, 'UserScript.require.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@resource` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'resource', + findMeta(aMeta, 'UserScript.resource.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@run-at` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'run-at', + findMeta(aMeta, 'UserScript.run-at.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@connect` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'connect', + findMeta(aMeta, 'UserScript.connect.value') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@antifeature` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'antifeature', + findMeta(aMeta, 'UserScript.antifeature.value1') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@noframes` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'noframes', + findMeta(aMeta, 'UserScript.noframes') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; + } + + aInnerCallback(null); + }, + function (aInnerCallback) { + // `@unwrap` validations + hasInvalidKey = scriptStorageLib.invalidKey( + userName, + scriptName, + isLib, + 'unwrap', + findMeta(aMeta, 'UserScript.unwrap') + ); + + if (hasInvalidKey) { + aInnerCallback(hasInvalidKey, null); + return; } aInnerCallback(null); @@ -1838,7 +2031,7 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { try { url = new URL(aRequire); - require = url.origin + url.pathname; + require = url.origin + url.pathname; } catch (aE) { // NOTE: Currently not always a real error in every .user.js engine so... /* falls through */ @@ -1866,6 +2059,9 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { function (aAlive, aScript, aRemoved) { var script = null; var s3 = null; + var now = null; + + var storeDescriptionLength = null; if (aRemoved) { aCallback(new statusError({ @@ -1882,16 +2078,24 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { } else if (!aScript && aUpdate) { aCallback(new statusError({ message: 'Updating but no script found.', - code: 500 // Status code unknown... could be user error too + code: 404 }), null); return; } else if (!aScript) { // New script + now = new Date(); + + + storeDescriptionLength = settings.scriptSearchQueryStoreMaxDescription; + storeDescriptionLength = rLogographic.test(thisDescription) + ? parseInt(storeDescriptionLength / logographicDivisor) + : storeDescriptionLength; + aScript = new Script({ name: thisName, _description: ( thisDescription - ? thisDescription.substr(0, settings.scriptSearchQueryStoreMaxDescription).trim() + ? thisDescription.substr(0, storeDescriptionLength).trim() : '' ), author: aUser.name, @@ -1899,7 +2103,8 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { rating: 0, about: '', _about: '', - updated: new Date(), + created: now, + updated: now, hash: crypto.createHash('sha512').update(aBuf).digest('hex'), votes: 0, flags: { critical: 0, absolute: 0 }, @@ -1930,9 +2135,15 @@ exports.storeScript = function (aUser, aMeta, aBuf, aUpdate, aCallback) { }), null); return; } + + storeDescriptionLength = settings.scriptSearchQueryStoreMaxDescription; + storeDescriptionLength = rLogographic.test(thisDescription) + ? parseInt(storeDescriptionLength / logographicDivisor) + : storeDescriptionLength; + aScript._description = ( thisDescription - ? thisDescription.substr(0, settings.scriptSearchQueryStoreMaxDescription).trim() + ? thisDescription.substr(0, storeDescriptionLength).trim() : '' ); aScript.meta = aMeta; @@ -2096,10 +2307,11 @@ exports.webhook = function (aReq, aRes) { var reponame = null; var repos = {}; var repo = null; + var update = null; // Return if script storage is in read-only mode if (process.env.READ_ONLY_SCRIPT_STORAGE === 'true') { - aRes.status(423).send(); // Locked + aRes.status(423).send('Resource is in lockdown.'); // Locked return; } @@ -2109,30 +2321,30 @@ exports.webhook = function (aReq, aRes) { // IPv6 address for caller will return `false` from dep in this // configuration if (githubHookAddresses.length < 1) { - aRes.status(502).send(); // Bad gateway + aRes.status(502).send('Authorized gateways absent.'); // Bad gateway return; } if (!ipRangeCheck(aReq.connection.remoteAddress, githubHookAddresses)) { - aRes.status(401).send(); // Unauthorized: No challenge and silent iterations + aRes.status(401).send('Unauthorized gateway.'); // Unauthorized: No challenge and silent iterations return; } // If media type is not corectly set up in GH webhook then reject // NOTE: Keep in sync with newScriptPage.html view if (!aReq.is('application/x-www-form-urlencoded')) { - aRes.status(415).send(); // Unsupported media type + aRes.status(415).send('Unsupported Content-Type for webhook.'); // Unsupported media type return; } if (!aReq.body.payload) { - aRes.status(502).send(); // Bad gateway + aRes.status(502).send('No initial payload.'); // Bad gateway return; } payload = JSON.parse(aReq.body.payload); if (!payload) { - aRes.status(400).send(); // Bad request + aRes.status(400).send('No parsed payload.'); // Bad request return; } @@ -2140,23 +2352,24 @@ exports.webhook = function (aReq, aRes) { case 'ping': // Initial setup of the webhook checks... informational if (!payload.hook && !payload.hook.events) { - aRes.status(502).send(); // Bad gateway e.g. something catastrophic to look into + aRes.status(502).send('Bad gateway.'); // Bad gateway e.g. something catastrophic to look into return; } if (payload.hook.events.length !== 1 || payload.hook.events.indexOf('push') !== 0) { - aRes.status(413).send(); // Payload (events) too large + aRes.status(413).send('Too many events.'); // Payload (events) too large return; } - aRes.status(200).send(); // Send acknowledgement for GH history + aRes.status(200).send('pong.'); // Send acknowledgement for GH history return; break; case 'push': // Pushing a change + update = aReq.get('X-GitHub-Delivery'); break; default: - aRes.status(400).send(); // Bad request + aRes.status(400).send('Unsupported event.'); // Bad request return; } @@ -2164,7 +2377,7 @@ exports.webhook = function (aReq, aRes) { // Only accept commits from the `master` branch if (payload.ref !== 'refs/heads/master') { - aRes.status(403).send(); // Forbidden + aRes.status(403).send('Default branch is not `master`.'); // Forbidden return; } @@ -2179,21 +2392,26 @@ exports.webhook = function (aReq, aRes) { var repoManager = null; if (aErr) { - aRes.status(500).send(); // Internal server error: Possibly 502 Bad gateway to DB or bad dep. + aRes.status(500).send('Internal server error.'); // Internal server error: Possibly 502 Bad gateway to DB or bad dep. return; } if (!aUser) { - aRes.status(400).send(); // Bad request: Possibly 410 Gone from DB but not GH + aRes.status(400).send('Bad request.'); // Bad request: Possibly 410 Gone from DB but not GH return; } if (!aUser.consented) { - aRes.status(451).send(); // Reject until consented + aRes.status(451).send('Consent is required for access.'); // Reject until consented return; } - aRes.status(202).send(); // Close connection with Accepted but processing + if (aUser.strategies.indexOf('github') <= -1) { // Don't rely on just `ghUsername`! + aRes.status(403).send('Requires supported authentication strategy on account.'); // Reject due to lack of GitHub as Auth + return; + } + + aRes.status(202).send('Your request is queued.'); // Close connection with Accepted but processing // Gather the modified user scripts payload.commits.forEach(function (aCommit) { @@ -2206,10 +2424,28 @@ exports.webhook = function (aReq, aRes) { // Update modified scripts repoManager = RepoManager.getManager(null, aUser, repos); - repoManager.loadScripts(true, function (aErr) { + + repoManager.loadSyncs(update, function (aErr) { if (aErr) { - console.error(aErr); + // These currently should be server side errors so always log + console.error(update, aErr); + return; } + + repoManager.loadScripts(update, function (aErr) { + + var code = null; + if (aErr) { + // Some errors could be user generated or dep generated user error, + // usually ignore those since handled with Sync model and visible + // to end user. We shouldn't be sending non-numeric codes. + code = (aErr instanceof statusError ? aErr.status.code : aErr.code); + if (code && !isNaN(code) && code >= 500) { + console.error(update, aErr); + } + } + }); }); + }); }; diff --git a/controllers/strategies.json b/controllers/strategies.json index 7ec165d17..1c37d5799 100644 --- a/controllers/strategies.json +++ b/controllers/strategies.json @@ -1,9 +1,4 @@ { - "facebook": { - "name": "Facebook", - "oauth": true, - "readonly": false - }, "github": { "name": "GitHub", "oauth": true, @@ -27,21 +22,11 @@ "reddit": { "name": "Reddit", "oauth": true, - "readonly": false + "readonly": true }, "steam": { "name": "Steam", "oauth": false, "readonly": false - }, - "twitter": { - "name": "Twitter", - "oauth": true, - "readonly": false - }, - "yahoo": { - "name": "Yahoo!", - "oauth": false, - "readonly": false } } diff --git a/controllers/user.js b/controllers/user.js index 55bfcae93..eeb9fe004 100644 --- a/controllers/user.js +++ b/controllers/user.js @@ -15,12 +15,17 @@ var async = require('async'); var _ = require('underscore'); var util = require('util'); var rfc2047 = require('rfc2047'); +var expressCaptcha = require('express-svg-captcha'); +var svgCaptcha = require('svg-captcha'); var SPDX = require('spdx-license-ids'); //--- Model inclusions -var Comment = require('../models/comment').Comment; var Script = require('../models/script').Script; +var Comment = require('../models/comment').Comment; +var Vote = require('../models/vote').Vote; +var Flag = require('../models/flag').Flag; +var Sync = require('../models/sync').Sync; var Strategy = require('../models/strategy').Strategy; var User = require('../models/user').User; var Discussion = require('../models/discussion').Discussion; @@ -39,6 +44,8 @@ var helpers = require('../libs/helpers'); var modelParser = require('../libs/modelParser'); var modelQuery = require('../libs/modelQuery'); +var scriptStorageLib = require('../libs/scriptStorage'); + var flagLib = require('../libs/flag'); var removeLib = require('../libs/remove'); var stats = require('../libs/stats'); @@ -46,11 +53,11 @@ var github = require('./../libs/githubClient'); var renderMd = require('../libs/markdown').renderMd; var getDefaultPagination = require('../libs/templateHelpers').getDefaultPagination; -var statusCodePage = require('../libs/templateHelpers').statusCodePage; var execQueryTask = require('../libs/tasks').execQueryTask; var countTask = require('../libs/tasks').countTask; var pageMetadata = require('../libs/templateHelpers').pageMetadata; var orderDir = require('../libs/templateHelpers').orderDir; +var statusCodePage = require('../libs/templateHelpers').statusCodePage; var getSessionDataList = require('../libs/modifySessions').getSessionDataList; var extendSession = require('../libs/modifySessions').extend; @@ -112,24 +119,53 @@ exports.exist = function (aReq, aRes) { aRes.set('Warning', msg); } - aRes.status(200).send(); + if (aUser._probationary) { + aRes.status(204).send(); + } else { + aRes.status(200).send(); + } }); }; // API - Request for extending a logged in user session exports.extend = function (aReq, aRes, aNext) { var authedUser = aReq.session.user; + var redirectTo = helpers.isSameOrigin(aReq.body.redirectTo); + + redirectTo = redirectTo.result ? redirectTo.URL : new URL('/', helpers.baseOrigin); User.findOne({ _id: authedUser._id, sessionIds: { "$in": [ aReq.sessionID ] } }, function (aErr, aUser) { - // WARNING: No err handling + if (aErr) { + statusCodePage(aReq, aRes, aNext, { + statusCode: aErr.code || 500, + statusMessage: aErr.message + }); + return; + } + + if (!aUser) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noextend'; + aRes.redirect(redirectTo); + return; + } + + if (aUser._probationary) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noextend'; + aRes.redirect(redirectTo); + return; + } extendSession(aReq, aUser, function (aErr) { if (aErr) { if (aErr === 'Already extended') { - aNext(); + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noextend'; + aRes.redirect(redirectTo); return; } @@ -140,7 +176,7 @@ exports.extend = function (aReq, aRes, aNext) { return; } - aRes.redirect('back'); + aRes.redirect(redirectTo); }); }); }; @@ -151,20 +187,21 @@ exports.destroyOne = function (aReq, aRes, aNext) { var authedUser = aReq.session.user; var username = aReq.body.username;; var id = aReq.body.id; + var redirectTo = helpers.isSameOrigin(aReq.body.redirectTo); + + redirectTo = redirectTo.result ? redirectTo.URL : new URL('/', helpers.baseOrigin); if (!username) { - statusCodePage(aReq, aRes, aNext, { - statusCode: 400, - statusMessage: 'username must be set.' - }); + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noname'; + aRes.redirect(redirectTo); return; } if (aReq.sessionID === id) { - statusCodePage(aReq, aRes, aNext, { - statusCode: 403, - statusMessage: 'Cannot delete the current session.' - }); + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'curses'; + aRes.redirect(redirectTo); return; } @@ -175,57 +212,65 @@ exports.destroyOne = function (aReq, aRes, aNext) { var store = aReq.sessionStore; var user = null; - if (aErr || !aUser) { - aNext(); - return; - } - - user = aUser; // NOTE: We really shouldn't need modelParser here - - if (authedUser.role > user.role) { + if (aErr) { + console.error(aErr); statusCodePage(aReq, aRes, aNext, { - statusCode: 403, - statusMessage: 'Cannot delete a session with a higher rank.' + statusCode: aErr.code, + statusMessage: aErr.message }); return; } - // You can only delete your own other sessions when you are not an admin - if (!authedUser.isAdmin && authedUser.name !== user.name) { - statusCodePage(aReq, aRes, aNext, { - statusCode: 403, - statusMessage: 'Cannot delete a session that is not owned.' - }); + if (aUser) { + user = aUser; // NOTE: We really shouldn't need modelParser here + + if (authedUser.role > user.role) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'hirank'; + aRes.redirect(redirectTo); + return; + } + + // You can only delete your own other sessions when you are not an admin + if (!authedUser.isAdmin && authedUser.name !== user.name) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noown'; + aRes.redirect(redirectTo); + return; + } + } else if (!authedUser.isAdmin) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'curses'; + aRes.redirect(redirectTo); return; } store.get(id, function (aErr, aSess) { if (aErr) { statusCodePage(aReq, aRes, aNext, { - statusCode: 500, - statusMessage: aErr + statusCode: aErr.code || 500, + statusMessage: aErr.message }); return; } - if (!authedUser.isAdmin && aSess.passport.oujsOptions.authFrom) { - statusCodePage(aReq, aRes, aNext, { - statusCode: 403, - statusMessage: 'Cannot delete a session that is being administered.' - }); + if (!authedUser.isAdmin && aSess.passport && aSess.passport.oujsOptions.authFrom) { + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'noadmin'; + aRes.redirect(redirectTo); return; } - destroyOneSession(aReq, user, id, function (aErr) { + destroyOneSession(aReq, authedUser.isAdmin, user, id, function (aErr) { if (aErr) { - statusCodePage(aReq, aRes, aNext, { - statusCode: 500, - statusMessage: aErr - }); + // NOTE: Watchpoint + redirectTo.search = (redirectTo.search ? redirectTo.search + '&' : '') + + 'curses'; + aRes.redirect(redirectTo); return; } - aRes.redirect('back'); + aRes.redirect(redirectTo); }); }); }); @@ -284,6 +329,9 @@ var getUserPageTasks = function (aOptions) { var user = null; var userScriptListCountQuery = null; var userCommentListCountQuery = null; + var userVoteListCountQuery = null; + var userFlagListCountQuery = null; + var userSyncListCountQuery = null; var tasks = []; // Shortcuts @@ -299,6 +347,18 @@ var getUserPageTasks = function (aOptions) { userCommentListCountQuery = Comment.find({ _authorId: user._id, flagged: { $ne: true } }); tasks.push(countTask(userCommentListCountQuery, aOptions, 'commentListCount')); + // userVoteListCountQuery + userVoteListCountQuery = Vote.find({ _userId: user._id }); + tasks.push(countTask(userVoteListCountQuery, aOptions, 'voteListCount')); + + // userFlagListCountQuery + userFlagListCountQuery = Flag.find({ _userId: user._id }); + tasks.push(countTask(userFlagListCountQuery, aOptions, 'flagListCount')); + + // userSyncListCountQuery + userSyncListCountQuery = Sync.find({ _authorId: user._id }); + tasks.push(countTask(userSyncListCountQuery, aOptions, 'syncListCount')); + return tasks; }; @@ -392,7 +452,7 @@ exports.userListPage = function (aReq, aRes, aNext) { async.parallel([ function (aCallback) { - if (!!!options.isFlagged || !options.isAdmin) { // NOTE: Watchpoint + if (!!!options.isFlagged || !options.isMod) { // NOTE: Watchpoint aCallback(); return; } @@ -421,7 +481,9 @@ exports.userListPage = function (aReq, aRes, aNext) { pageMetadata(options, 'Users'); // Order dir - orderDir(aReq, options, 'name', 'desc'); + orderDir(aReq, options, 'name', 'asc'); + orderDir(aReq, options, 'created', 'desc'); + orderDir(aReq, options, 'updated', 'desc'); orderDir(aReq, options, 'role', 'asc'); // userListQuery @@ -430,6 +492,14 @@ exports.userListPage = function (aReq, aRes, aNext) { // userListQuery: Defaults modelQuery.applyUserListQueryDefaults(userListQuery, options, aReq); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // userListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -457,6 +527,10 @@ exports.view = function (aReq, aRes, aNext) { } function render() { + // Set crawlers to ignore for indexing. Following is currently managed in markdown rendering. + // Because we use common html across multiple pages meta tags shouldn't be used. + aRes.set('X-Robots-Tag', 'noindex'); + aRes.render('pages/userPage', options); } @@ -464,7 +538,7 @@ exports.view = function (aReq, aRes, aNext) { async.parallel([ function (aCallback) { - if (!options.isAdmin) { // NOTE: Watchpoint + if (!options.isMod) { // NOTE: Watchpoint aCallback(); return; } @@ -627,6 +701,14 @@ exports.userCommentListPage = function (aReq, aRes, aNext) { modelQuery.applyCommentListQueryDefaults(commentListQuery, options, aReq); commentListQuery.sort('-created'); + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // commentListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults @@ -637,7 +719,7 @@ exports.userCommentListPage = function (aReq, aRes, aNext) { }); // SearchBar - options.searchBarPlaceholder = 'Search Comments from ' + user.name; + options.searchBarPlaceholder = options.authRequired + 'Search Comments from ' + user.name; options.searchBarFormAction = ''; //--- Tasks @@ -696,7 +778,7 @@ exports.userScriptListPage = function (aReq, aRes, aNext) { async.parallel([ function (aCallback) { - if (!!!options.isFlagged || !options.isAdmin) { // NOTE: Watchpoint + if (!!!options.isFlagged || !options.isMod) { // NOTE: Watchpoint aCallback(); return; } @@ -811,12 +893,20 @@ exports.userScriptListPage = function (aReq, aRes, aNext) { modelQuery.applyScriptListQueryDefaults(scriptListQuery, options, aReq); } + if (options.authToSearch) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, // Forbidden + statusMessage: 'Please Sign In to Search' + }); + return; + } + // scriptListQuery: Pagination pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults // SearchBar - options.searchBarPlaceholder = 'Search ' + - (options.librariesOnly ? 'Libraries' : 'Scripts') + ' from ' + user.name; + options.searchBarPlaceholder = options.authRequired + 'Search ' + + (librariesOnly ? 'Libraries' : 'Scripts') + ' from ' + user.name; options.searchBarFormAction = ''; //--- Tasks @@ -838,6 +928,115 @@ exports.userScriptListPage = function (aReq, aRes, aNext) { }); }; +exports.userSyncListPage = function (aReq, aRes, aNext) { + // + var username = aReq.params.username; + + User.findOne({ + name: caseInsensitive(username) + }, function (aErr, aUser) { + function preRender() { + // syncList + options.syncList = _.map(options.syncList, modelParser.parseSync); + + // Pagination + options.paginationRendered = pagination.renderDefault(aReq); + } + + function render() { + aRes.render('pages/userSyncListPage', options); + } + + function asyncComplete() { + preRender(); + render(); + } + + // + var options = {}; + var authedUser = aReq.session.user; + var user = null; + var syncListQuery = null; + var pagination = null; + var tasks = []; + + if (aErr || !aUser) { + aNext(); + return; + } + + // Session + options.authedUser = authedUser = modelParser.parseUser(authedUser); + options.isMod = authedUser && authedUser.isMod; + options.isAdmin = authedUser && authedUser.isAdmin; + + // User + user = options.user = modelParser.parseUser(aUser); + options.isYou = authedUser && user && authedUser._id == user._id; + + // If not you or not synacable auth strategy move along + if (!(options.isYou || options.isAdmin) || !options.user.canSync) { + aNext(); + return; + } + + // Page metadata + pageMetadata(options, [user.name, 'Users']); + options.isUserSyncListPage = true; + + // Order dir + orderDir(aReq, options, 'target', 'desc'); + orderDir(aReq, options, 'created', 'asc'); + orderDir(aReq, options, 'updated', 'asc'); + orderDir(aReq, options, 'response', 'desc'); + + // SyncListQuery + syncListQuery = Sync.find(); + + // syncListQuery: author=user + syncListQuery.find({ _authorId: user._id }); + + // syncListQuery: Defaults + modelQuery.applySyncListQueryDefaults(syncListQuery, options, aReq); + + // syncListQuery: Pagination + pagination = options.pagination; // is set in modelQuery.apply___ListQueryDefaults + + // SearchBar + options.searchBarPlaceholder = 'Search Syncs from ' + user.name; + options.searchBarFormAction = ''; + + //--- Tasks + + // Pagination + tasks.push(pagination.getCountTask(syncListQuery)); + + // syncListQuery + tasks.push(execQueryTask(syncListQuery, options, 'syncList')); + + // UserPage tasks + tasks = tasks.concat(getUserPageTasks(options)); + + //-- + async.parallel(tasks, asyncComplete); + }); +}; + +var captcha = new expressCaptcha(settings.captchaOpts); + +exports.userEditProfilePageCaptcha = function (aReq, aRes, aNext) { + var authedUser = aReq.session.user; + var username = aReq.params.username; + + + if (authedUser.slugUrl === username) { + (captcha.generate())(aReq, aRes, aNext); + } else { + aRes.set('X-Robots-Tag', 'noindex, nofollow'); + aRes.type('svg').status(200).send(svgCaptcha('3.14 x 2.71 / 0', settings.captchaOpts)); + } +} + exports.userEditProfilePage = function (aReq, aRes, aNext) { var authedUser = aReq.session.user; @@ -876,6 +1075,8 @@ exports.userEditProfilePage = function (aReq, aRes, aNext) { options.user = user = modelParser.parseUser(aUser); options.isYou = authedUser && user && authedUser._id == user._id; + options.user.hasCaptcha = true; + // Page metadata pageMetadata(options, [user.name, 'Users']); @@ -934,6 +1135,8 @@ exports.userEditPreferencesPage = function (aReq, aRes, aNext) { var scriptListQuery = null; var tasks = []; + var thisURL = null; + if (aErr || !aUser) { aNext(); return; @@ -948,6 +1151,14 @@ exports.userEditPreferencesPage = function (aReq, aRes, aNext) { options.user = user = modelParser.parseUser(aUser); options.isYou = authedUser && user && authedUser._id == user._id; + // redirectTo (forced) + thisURL = new URL(aReq.url, helpers.baseOrigin); + ['noname', 'curses', 'hirank', 'noown', 'noadmin', 'noextend'] + .forEach(function (aE, aI, aA) { + thisURL.searchParams.delete(aE); + }); + options.redirectToo = thisURL.pathname + (thisURL.search ? thisURL.search : ''); + // Page metadata pageMetadata(options, [user.name, 'Users']); @@ -992,12 +1203,14 @@ exports.userEditPreferencesPage = function (aReq, aRes, aNext) { if (userStrats.indexOf(aStrat.name) > -1) { options.usedStrategies.push({ 'strat': aStrat.name, - 'display': aStrat.display + 'display': aStrat.display, + 'disabled': strategies[aStrat.name] ? strategies[aStrat.name].readonly : true }); } else { options.openStrategies.push({ 'strat': aStrat.name, - 'display': aStrat.display + 'display': aStrat.display, + 'disabled': strategies[aStrat.name] ? strategies[aStrat.name].readonly : true }); } }); @@ -1010,12 +1223,14 @@ exports.userEditPreferencesPage = function (aReq, aRes, aNext) { if (userStrats.indexOf(name) > -1) { options.usedStrategies.push({ 'strat': name, - 'display': strategy.name + 'display': strategy.name, + 'disabled': strategy.readonly }); } else { options.openStrategies.push({ 'strat': name, - 'display': strategy.name + 'display': strategy.name, + 'disabled': strategy.readonly }); } } @@ -1029,6 +1244,9 @@ exports.userEditPreferencesPage = function (aReq, aRes, aNext) { options.defaultStrategy = strategies[defaultStrategy] ? strategies[defaultStrategy].name : null; + options.defaultStrategyDisabled = strategies[defaultStrategy] + ? strategies[defaultStrategy].readonly + : true; options.defaultStrat = defaultStrategy; options.haveOtherStrategies = options.usedStrategies.length > 0; @@ -1146,12 +1364,31 @@ exports.userGitHubRepoListPage = function (aReq, aRes, aNext) { } function asyncComplete(aErr) { + var msg = null; + if (aErr) { - console.error(aErr); - statusCodePage(aReq, aRes, aNext, { - statusCode: 500, - statusMessage: 'Server Error' - }); + switch (aErr.code) { // NOTE: Important to test for GH code vs potential OUJS code + case 401: + // fallsthrough + case 403: + try { + msg = JSON.parse(aErr.message); + } catch (aE) { + msg = { message: aErr.message }; + } + console.warn(msg.message); + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + break; + default: + console.error(aErr); + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: 'Server Error' + }); + } return; } @@ -1172,6 +1409,22 @@ exports.userGitHubRepoListPage = function (aReq, aRes, aNext) { options.isAdmin = authedUser && authedUser.isAdmin; // GitHub + if (!options.authedUser.hasGithub) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, + statusMessage: 'You do not have GitHub as an auth strategy.' + }); + return; + } + + if (process.env.DISABLE_SCRIPT_IMPORT === 'true') { + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + return; + } + options.githubUserId = githubUserId = aReq.query.user || authedUser.ghUsername || authedUser.githubUserId(); @@ -1257,8 +1510,31 @@ exports.userGitHubRepoPage = function (aReq, aRes, aNext) { } function asyncComplete(aErr) { + var msg = null; + if (aErr) { - aNext(); + switch (aErr.code) { // NOTE: Important to test for GH code vs potential OUJS code + case 401: + // fallsthrough + case 403: + try { + msg = JSON.parse(aErr.message); + } catch (aE) { + msg = { message: aErr.message }; + } + console.warn(msg.message); + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + break; + default: + console.error(aErr); + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: 'Server Error' + }); + } return; } @@ -1279,6 +1555,22 @@ exports.userGitHubRepoPage = function (aReq, aRes, aNext) { options.isAdmin = authedUser && authedUser.isAdmin; // GitHub + if (!options.authedUser.hasGithub) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, + statusMessage: 'You do not have GitHub as an auth strategy.' + }); + return; + } + + if (process.env.DISABLE_SCRIPT_IMPORT === 'true') { + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + return; + } + options.githubUserId = githubUserId = aReq.query.user || authedUser.ghUsername || authedUser.githubUserId(); @@ -1386,6 +1678,22 @@ exports.userGitHubImportScriptPage = function (aReq, aRes, aNext) { options.isOwnRepo = authedUser.ghUsername && authedUser.ghUsername === options.githubUserId; + if (!options.authedUser.hasGithub) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 403, + statusMessage: 'You do not have GitHub as an auth strategy.' + }); + return; + } + + if (process.env.DISABLE_SCRIPT_IMPORT === 'true') { + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + return; + } + if (!options.isOwnRepo) { statusCodePage(aReq, aRes, aNext, { statusCode: 403, @@ -1561,32 +1869,54 @@ exports.userGitHubImportScriptPage = function (aReq, aRes, aNext) { }, ], function (aErr) { var script = null; + var code = null; + var msg = null; if (aErr) { - console.error([ - aErr, - authedUser.name + ' ' + githubUserId + ' ' + githubRepoName + ' ' + githubBlobPath + code = (aErr instanceof statusError ? aErr.status.code : aErr.code); + if (code && !isNaN(code) && code >= 500) { + console.error([ + aErr, + authedUser.name + ' ' + githubUserId + ' ' + githubRepoName + ' ' + githubBlobPath - ].join('\n')); + ].join('\n')); + } if (!(aErr instanceof String)) { - statusCodePage(aReq, aRes, aNext, { - statusCode: (aErr instanceof statusError ? aErr.status.code : aErr.code), - statusMessage: (aErr instanceof statusError ? aErr.status.message : aErr.message), - isCustomView: true, - statusData: { - isGHImport: true, - utf_pathname: githubPathName, - utf_pathext: githubPathExt, - user: encodeURIComponent(githubUserId), - repo: encodeURIComponent(githubRepoName), - default_branch: encodeURIComponent(githubDefaultBranch), - path: encodeURIComponent(githubBlobPath) - } - }); + switch (aErr.code) { // NOTE: Important to test for GH code vs potential OUJS code + case 401: + // fallsthrough + case 403: + try { + msg = JSON.parse(aErr.message); + } catch (aE) { + msg = { message: aErr.message }; + } + console.warn(msg.message); + statusCodePage(aReq, aRes, aNext, { + statusCode: 503, + statusMessage: 'Service unavailable. Please check back later.' + }); + break; + default: + statusCodePage(aReq, aRes, aNext, { + statusCode: (aErr instanceof statusError ? aErr.status.code : aErr.code), + statusMessage: (aErr instanceof statusError ? aErr.status.message : aErr.message), + isCustomView: true, + statusData: { + isGHImport: true, + utf_pathname: githubPathName, + utf_pathext: githubPathExt, + user: encodeURIComponent(githubUserId), + repo: encodeURIComponent(githubRepoName), + default_branch: encodeURIComponent(githubDefaultBranch), + path: encodeURIComponent(githubBlobPath) + } + }); + } } else { statusCodePage(aReq, aRes, aNext, { - statusCode: 500, // NOTE: Watchpoint + statusCode: 500, statusMessage: aErr }); } @@ -1595,7 +1925,7 @@ exports.userGitHubImportScriptPage = function (aReq, aRes, aNext) { script = modelParser.parseScript(options.script); - aRes.redirect(script.scriptPageUri); + aRes.redirect(script.scriptEditMetadataPageUri); }); }; @@ -1660,14 +1990,59 @@ exports.uploadScript = function (aReq, aRes, aNext) { form = new formidable.IncomingForm(); form.parse(aReq, function (aErr, aFields, aFiles) { - // WARNING: No err handling + var msg = null; + + if (aErr) { + if (aErr) { + msg = 'Unknown error when form parsing at `uploadScript`.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development.'].join(' ') + }); + console.error(aErr); + return; + } + } + + if (!aFields || (aFields && aFields.uploadScript && !aFields.uploadScript[0])) { + msg = '`uploadScript` field is missing.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + console.error(msg); + return; + } + + if (aFields.uploadScript[0] !== 'true') { + msg = '`uploadScript` field is invalid :='; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, aFields.uploadScript].join(' ') + }); + console.error([msg, aFields.uploadScript].join(' ')); + return; + } + + // TODO: Maybe add more fields validation + + if (!aFiles) { + msg = 'Upload Script is missing File.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + console.error(msg); + return; + } // var isLib = aReq.params.isLib; - var script = aFiles.script; + var script = aFiles.script && aFiles.script[0] ? aFiles.script[0] : null; var stream = null; var bufs = []; var authedUser = aReq.session.user; + var msg = null; // Reject missing files if (!script) { @@ -1680,9 +2055,9 @@ exports.uploadScript = function (aReq, aRes, aNext) { // Reject non-js file if (isDev) { - console.log('Upload Script MIME Content-Type is `' + script.type + '`'); + console.log('Upload Script MIME Content-Type is `' + script.mimetype + '`'); } - switch (script.type) { + switch (script.mimetype) { case 'application/x-javascript': // #872 #1661 case 'application/javascript': // #1599 case 'text/javascript': // Default @@ -1697,36 +2072,71 @@ exports.uploadScript = function (aReq, aRes, aNext) { // Reject huge file if (script.size > settings.maximum_upload_script_size) { + msg = util.format('Selected file size is larger than maximum (%s bytes).', + settings.maximum_upload_script_size) statusCodePage(aReq, aRes, aNext, { statusCode: 400, - statusMessage: 'Selected file is too big.' + statusMessage: msg }); return; } - stream = fs.createReadStream(script.path); + stream = fs.createReadStream(script.filepath); + + stream.on('error', function(aErr) { + msg = 'Upload Script failed.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + console.error(aErr); + return; + }); + stream.on('data', function (aData) { bufs.push(aData); }); stream.on('end', function () { User.findOne({ _id: authedUser._id }, function (aErr, aUser) { - // WARNING: No err handling + var msg = null; + + if (aErr) { + msg = 'Unknown error when finding User at processing Script stream.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development.'].join(' ') + }); + console.error(aErr); + return; + } + + if (!aUser) { + msg = 'No user found.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + return; + } var bufferConcat = Buffer.concat(bufs); scriptStorage.getMeta(bufs, function (aMeta) { + var msg = null; + if (!isLib && !!scriptStorage.findMeta(aMeta, 'UserLibrary')) { + msg = 'UserLibrary metadata block found while attempting to upload as a UserScript.'; statusCodePage(aReq, aRes, aNext, { statusCode: 400, - statusMessage: - 'UserLibrary metadata block found while attempting to upload as a UserScript.' + statusMessage: msg }); return; } else if (isLib && !!!scriptStorage.findMeta(aMeta, 'UserLibrary')) { + msg = 'UserLibrary metadata block missing.'; statusCodePage(aReq, aRes, aNext, { statusCode: 400, - statusMessage: 'UserLibrary metadata block missing.' + statusMessage: msg }); return; } @@ -1744,7 +2154,8 @@ exports.uploadScript = function (aReq, aRes, aNext) { '/' + (isLib ? 'libs' : 'scripts') + '/' + encodeURIComponent(helpers.cleanFilename(aScript.author)) + '/' + - encodeURIComponent(helpers.cleanFilename(aScript.name)) + encodeURIComponent(helpers.cleanFilename(aScript.name)) + + (aScript._about !== '' ? '' : '/edit') ); }); }); @@ -1759,16 +2170,57 @@ exports.update = function (aReq, aRes, aNext) { var authedUser = aReq.session.user; // Update the about section of a user's profile - User.findOneAndUpdate({ _id: authedUser._id }, { about: aReq.body.about }, - function (aErr, aUser) { + User.findOne({ _id: authedUser._id }, function (aErr, aUser) { + if (aErr) { + aRes.redirect('/'); + return; + } + + if (!aUser) { + msg = 'No user found.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + return; + } + + if (!captcha.validate(aReq, aReq.body.captcha)) { + aRes.redirect('/users/' + encodeURIComponent(aUser.name)); + return; + } + + // Update DB + aUser.updated = new Date(); + aUser.about = aReq.body.about; + aUser.save(function (aErr, aUser) { + var msg = null; + if (aErr) { - aRes.redirect('/'); + msg = 'Unknown error when saving Profile.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development'].join(' ') + }); + console.error(aErr); + return; + } + if (!aUser) { + msg = 'No user handle when saving Profile.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development'].join(' ') + }); + console.error(msg) return; } + // Update session authedUser.about = aUser.about; + aRes.redirect('/users/' + encodeURIComponent(aUser.name)); }); + }); }; // Submit a script through the web editor @@ -1781,15 +2233,39 @@ exports.submitSource = function (aReq, aRes, aNext) { function storeScript(aMeta, aSource) { User.findOne({ _id: authedUser._id }, function (aErr, aUser) { - // WARNING: No err handling + var msg = null; + + if (aErr) { + msg = 'Unknown error when finding User at `submitSource`.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development.'].join(' ') + }); + console.error(aErr); + return; + } + + if (!aUser) { + msg = 'No user found.' + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: msg + }); + return; + } scriptStorage.storeScript(aUser, aMeta, aSource, false, function (aErr, aScript) { - var redirectUri = aScript - ? ((aScript.isLib ? '/libs/' : '/scripts/') + - encodeURIComponent(helpers.cleanFilename(aScript.author)) + - '/' + - encodeURIComponent(helpers.cleanFilename(aScript.name))) - : aReq.body.url; + var msg = null; + + var redirectUri = ( + aScript + ? ((aScript.isLib ? '/libs/' : '/scripts/') + + encodeURIComponent(helpers.cleanFilename(aScript.author)) + + '/' + + encodeURIComponent(helpers.cleanFilename(aScript.name))) + + (aScript._about !== '' ? '' : '/edit') + : aReq.body.url // NOTE: Watchpoint + ); if (aErr) { statusCodePage(aReq, aRes, aNext, { @@ -1800,9 +2276,10 @@ exports.submitSource = function (aReq, aRes, aNext) { } if (!aScript) { + msg = 'No script found.'; statusCodePage(aReq, aRes, aNext, { - statusCode: 500, // NOTE: Watch point - statusMessage: 'No script' + statusCode: 500, + statusMessage: msg }); return; } @@ -1834,7 +2311,26 @@ exports.submitSource = function (aReq, aRes, aNext) { aScript.fork = fork; aScript.save(function (aErr, aScript) { - // WARNING: No err handling + var msg = null; + + if (aErr) { + msg = 'Unknown error when saving at `submitSource`.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development'].join(' ') + }); + console.error(aErr); + return; + } + if (!aScript) { + msg = 'No script handle when saving at `submitSource`.'; + statusCodePage(aReq, aRes, aNext, { + statusCode: 500, + statusMessage: [msg, 'Please contact Development'].join(' ') + }); + console.error(msg) + return; + } aRes.redirect(redirectUri); }); @@ -2042,7 +2538,7 @@ exports.editScript = function (aReq, aRes, aNext) { var installNameBase = null; var isLib = aReq.params.isLib; var tasks = []; - var nowDate = null; + var now = null; // Session options.authedUser = authedUser = modelParser.parseUser(authedUser); @@ -2065,6 +2561,11 @@ exports.editScript = function (aReq, aRes, aNext) { }); }); + // Lockdown + options.lockdown = {}; + options.lockdown.scriptStorageRO = process.env.READ_ONLY_SCRIPT_STORAGE === 'true'; + options.lockdown.updateURLCheck = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; + if (!isNew) { installNameBase = scriptStorage.getInstallNameBase(aReq); @@ -2081,7 +2582,10 @@ exports.editScript = function (aReq, aRes, aNext) { var licensePrimary = null; var copyrights = null; var copyrightPrimary = null; - var sinceDate = null; + var createdDate = null; + var tryURL = null; + var tryInstallNameBase = null; + var hasInvalidKey = null; //--- if (aErr || !aScript) { @@ -2089,11 +2593,6 @@ exports.editScript = function (aReq, aRes, aNext) { return; } - // Lockdown - options.lockdown = {}; - options.lockdown.scriptStorageRO = process.env.READ_ONLY_SCRIPT_STORAGE === 'true'; - options.lockdown.updateURLCheck = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; - // Script options.script = script = modelParser.parseScript(aScript); @@ -2116,14 +2615,37 @@ exports.editScript = function (aReq, aRes, aNext) { script.copyrightPrimary = copyrightPrimary; } else { if (authedUser) { - sinceDate = new Date(script._sinceISOFormat); - script.copyrightPrimary = sinceDate.getFullYear() + ', ' + authedUser.name + + createdDate = new Date(script.createdISOFormat); + script.copyrightPrimary = createdDate.getFullYear() + ', ' + authedUser.name + ' (' + helpers.baseOrigin + authedUser.userPageUrl + ')'; } } options.isOwner = authedUser && (authedUser._id == script._authorId || collaborators.indexOf(authedUser.name) > -1); + + hasInvalidKey = scriptStorageLib.invalidKey( + aScript.author, + aScript.name, + aScript.isLib, + 'updateURL', + scriptStorage.findMeta(aScript.meta, 'UserScript.updateURL.0.value') + ); + + if (!options.isOwner && !options.isMod && hasInvalidKey) { + statusCodePage(aReq, aRes, aNext, { + statusCode: (hasInvalidKey instanceof statusError + ? hasInvalidKey.status.code + : hasInvalidKey.code + ), + statusMessage: (hasInvalidKey instanceof statusError + ? hasInvalidKey.status.message + : hasInvalidKey.message + ) + }); + return; + } + modelParser.renderScript(script); script.installNameSlug = installNameBase; @@ -2132,11 +2654,25 @@ exports.editScript = function (aReq, aRes, aNext) { script.scriptPermalinkInstallPageUrlMin = 'https://' + aReq.get('host') + script.scriptInstallPageXUrl + ".min.user.js"; + tryInstallNameBase = scriptStorage.getInstallNameBase(aReq); + + try { + tryURL = new URL('../' + tryInstallNameBase, 'https://example.org/'); + + if ( + decodeURIComponent(tryURL.toString()) !== 'https://example.org/' + tryInstallNameBase + ) { + tryInstallNameBase = scriptStorage.getInstallNameBase(aReq, { encoding: 'uri' }); + } + } catch (aE) { + tryInstallNameBase = scriptStorage.getInstallNameBase(aReq, { encoding: 'uri' }); + } + script.scriptRawPageUrl = '/src/' + (isLib ? 'libs' : 'scripts') + '/' + - scriptStorage.getInstallNameBase(aReq, { encoding: 'uri' }) + + tryInstallNameBase + (isLib ? '.js' : '.user.js'); script.scriptRawPageXUrl = '/src/' + (isLib ? 'libs' : 'scripts') + '/' + - scriptStorage.getInstallNameBase(aReq, { encoding: 'uri' }) + + tryInstallNameBase + (isLib ? '.min.js' : '.min.user.js'); script.scriptPermalinkRawPageUrl = 'https://' + aReq.get('host') + @@ -2224,8 +2760,8 @@ exports.editScript = function (aReq, aRes, aNext) { options.script.licensePrimary = 'MIT'; // NOTE: Site default if (authedUser) { - nowDate = new Date(); - options.script.copyrightPrimary = nowDate.getFullYear() + ', ' + authedUser.name + + now = new Date(); + options.script.copyrightPrimary = now.getFullYear() + ', ' + authedUser.name + ' (' + helpers.baseOrigin + authedUser.userPageUrl + ')'; } diff --git a/controllers/vote.js b/controllers/vote.js index 746ee05ef..c12a6dcaa 100644 --- a/controllers/vote.js +++ b/controllers/vote.js @@ -45,7 +45,7 @@ exports.vote = function (aReq, aRes, aNext) { form.parse(aReq, function (aErr, aFields) { // WARNING: No err handling - var vote = aFields.vote; + var vote = aFields.vote && aFields.vote[0] ? aFields.vote[0] : null; var unvote = false; var type = aReq.params[0]; @@ -56,9 +56,9 @@ exports.vote = function (aReq, aRes, aNext) { switch (vote) { case 'up': - // fallthrough + // fallsthrough case 'down': - // fallthrough + // fallsthrough case 'un': break; default: diff --git a/dev/devDBclean.gz b/dev/devDBclean.gz new file mode 100644 index 000000000..24ef030d2 Binary files /dev/null and b/dev/devDBclean.gz differ diff --git a/dev/devDBdirty.gz b/dev/devDBdirty.gz new file mode 100644 index 000000000..1cfe23002 Binary files /dev/null and b/dev/devDBdirty.gz differ diff --git a/dev/preinstall.js b/dev/preinstall.js index 2f73abefd..0580b1f21 100644 --- a/dev/preinstall.js +++ b/dev/preinstall.js @@ -1,10 +1,5 @@ 'use strict'; -// Define some pseudo module globals -var isPro = require('../libs/debug').isPro; -var isDev = require('../libs/debug').isDev; -var isDbg = require('../libs/debug').isDbg; - // // NOTE: Only use native *node* `require`s in this file // since dependencies may not be installed yet diff --git a/libs/blockSPDX.json b/libs/blockSPDX.json index ee8c7f780..4e11d69d3 100644 --- a/libs/blockSPDX.json +++ b/libs/blockSPDX.json @@ -1,95 +1,146 @@ [ "AAL", - "Abstyles", - "Adobe-2006", - "Adobe-Glyph", "ADSL", "AFL-1.1", "AFL-1.2", "AFL-2.0", "AFL-2.1", "AFL-3.0", - "Afmparse", - "AGPL-1.0", "AGPL-1.0-only", "AGPL-1.0-or-later", - "AGPL-1.0", - "Aladdin", "AMDPLPA", "AML", + "AML-glslang", "AMPAS", "ANTLR-PD", - "Apache-1.0", - "Apache-1.1", + "ANTLR-PD-fallback", "APAFML", "APSL-1.0", "APSL-1.1", "APSL-1.2", "APSL-2.0", - "Artistic-1.0-cl8", - "Artistic-1.0-Perl", + "ASWF-Digital-Assets-1.0", + "ASWF-Digital-Assets-1.1", + "Abstyles", + "AdaCore-doc", + "Adobe-2006", + "Adobe-Display-PostScript", + "Adobe-Glyph", + "Adobe-Utopia", + "Afmparse", + "Aladdin", + "Apache-1.0", + "Apache-1.1", + "App-s2p", + "Arphic-1999", "Artistic-1.0", - "Bahyph", - "Barr", - "Beerware", - "BitTorrent-1.0", - "BitTorrent-1.1", - "blessing", - "BlueOak-1.0.0", - "Borceux", + "Artistic-1.0-Perl", + "Artistic-1.0-cl8", "BSD-1-Clause", - "BSD-2-Clause-FreeBSD", - "BSD-2-Clause-NetBSD", + "BSD-2-Clause-Darwin", "BSD-2-Clause-Patent", + "BSD-2-Clause-Views", "BSD-3-Clause-Attribution", "BSD-3-Clause-Clear", + "BSD-3-Clause-HP", "BSD-3-Clause-LBNL", - "BSD-3-Clause-No-Nuclear-License-2014", + "BSD-3-Clause-Modification", + "BSD-3-Clause-No-Military-License", "BSD-3-Clause-No-Nuclear-License", + "BSD-3-Clause-No-Nuclear-License-2014", "BSD-3-Clause-No-Nuclear-Warranty", "BSD-3-Clause-Open-MPI", - "BSD-4-Clause-UC", + "BSD-3-Clause-Sun", + "BSD-3-Clause-acpica", + "BSD-3-Clause-flex", "BSD-4-Clause", + "BSD-4-Clause-Shortened", + "BSD-4-Clause-UC", + "BSD-4.3RENO", + "BSD-4.3TAHOE", + "BSD-Advertising-Acknowledgement", + "BSD-Attribution-HPND-disclaimer", + "BSD-Inferno-Nettverk", "BSD-Protection", "BSD-Source-Code", - "bzip2-1.0.5", - "bzip2-1.0.6", - "Caldera", + "BSD-Source-beginning-file", + "BSD-Systemics", + "BSD-Systemics-W3Works", + "BUSL-1.1", + "Baekmuk", + "Bahyph", + "Barr", + "Beerware", + "BitTorrent-1.0", + "BitTorrent-1.1", + "Bitstream-Charter", + "Bitstream-Vera", + "BlueOak-1.0.0", + "Boehm-GC", + "Borceux", + "Brian-Gladman-2-Clause", + "Brian-Gladman-3-Clause", + "C-UDA-1.0", + "CAL-1.0", + "CAL-1.0-Combined-Work-Exception", "CATOSL-1.1", "CC-BY-1.0", "CC-BY-2.0", "CC-BY-2.5", + "CC-BY-2.5-AU", "CC-BY-3.0", + "CC-BY-3.0-AT", + "CC-BY-3.0-AU", + "CC-BY-3.0-DE", + "CC-BY-3.0-IGO", + "CC-BY-3.0-NL", + "CC-BY-3.0-US", "CC-BY-4.0", "CC-BY-NC-1.0", "CC-BY-NC-2.0", "CC-BY-NC-2.5", "CC-BY-NC-3.0", + "CC-BY-NC-3.0-DE", "CC-BY-NC-4.0", "CC-BY-NC-ND-1.0", "CC-BY-NC-ND-2.0", "CC-BY-NC-ND-2.5", "CC-BY-NC-ND-3.0", + "CC-BY-NC-ND-3.0-DE", + "CC-BY-NC-ND-3.0-IGO", "CC-BY-NC-ND-4.0", "CC-BY-NC-SA-1.0", "CC-BY-NC-SA-2.0", + "CC-BY-NC-SA-2.0-DE", + "CC-BY-NC-SA-2.0-FR", + "CC-BY-NC-SA-2.0-UK", "CC-BY-NC-SA-2.5", "CC-BY-NC-SA-3.0", + "CC-BY-NC-SA-3.0-DE", + "CC-BY-NC-SA-3.0-IGO", "CC-BY-NC-SA-4.0", "CC-BY-ND-1.0", "CC-BY-ND-2.0", "CC-BY-ND-2.5", "CC-BY-ND-3.0", + "CC-BY-ND-3.0-DE", "CC-BY-ND-4.0", "CC-BY-SA-1.0", "CC-BY-SA-2.0", + "CC-BY-SA-2.0-UK", + "CC-BY-SA-2.1-JP", "CC-BY-SA-2.5", "CC-BY-SA-3.0", + "CC-BY-SA-3.0-AT", + "CC-BY-SA-3.0-DE", + "CC-BY-SA-3.0-IGO", "CC-BY-SA-4.0", "CC-PDDC", "CC0-1.0", "CDDL-1.1", + "CDL-1.0", "CDLA-Permissive-1.0", + "CDLA-Permissive-2.0", "CDLA-Sharing-1.0", "CECILL-1.0", "CECILL-1.1", @@ -98,84 +149,142 @@ "CECILL-C", "CERN-OHL-1.1", "CERN-OHL-1.2", - "ClArtistic", + "CERN-OHL-P-2.0", + "CERN-OHL-S-2.0", + "CERN-OHL-W-2.0", + "CFITSIO", + "CMU-Mach", + "CMU-Mach-nodoc", "CNRI-Jython", - "CNRI-Python-GPL-Compatible", "CNRI-Python", - "Condor-1.1", - "copyleft-next-0.3.0", - "copyleft-next-0.3.1", + "CNRI-Python-GPL-Compatible", + "COIL-1.0", "CPL-1.0", "CPOL-1.02", + "CUA-OPL-1.0", + "Caldera", + "Caldera-no-preamble", + "ClArtistic", + "Clips", + "Community-Spec-1.0", + "Condor-1.1", + "Cornell-Lossless-JPEG", + "Cronyx", "Crossword", "CrystalStacker", - "CUA-OPL-1.0", "Cube", - "curl", "D-FSL-1.0", - "diffmark", + "DEC-3-Clause", + "DL-DE-BY-2.0", + "DL-DE-ZERO-2.0", "DOC", - "Dotseqn", + "DRL-1.0", + "DRL-1.1", "DSDP", - "dvipdfm", + "Dotseqn", "ECL-1.0", "EFL-1.0", "EFL-2.0", - "eGenix", - "Entessa", + "EPICS", "EPL-1.0", "EPL-2.0", - "ErlPL-1.1", "EUDatagrid", "EUPL-1.0", + "Elastic-2.0", + "Entessa", + "ErlPL-1.1", "Eurosym", - "Fair", - "Frameworx-1.0", - "FreeImage", + "FBM", + "FDK-AAC", "FSFAP", + "FSFAP-no-warranty-disclaimer", "FSFUL", "FSFULLR", + "FSFULLRWD", "FTL", + "Fair", + "Ferguson-Twofish", + "Frameworx-1.0", + "FreeBSD-DOC", + "FreeImage", + "Furuseth", + "GCR-docs", + "GD", + "GFDL-1.1-invariants-only", + "GFDL-1.1-invariants-or-later", + "GFDL-1.1-no-invariants-only", + "GFDL-1.1-no-invariants-or-later", "GFDL-1.1-only", "GFDL-1.1-or-later", + "GFDL-1.2-invariants-only", + "GFDL-1.2-invariants-or-later", + "GFDL-1.2-no-invariants-only", + "GFDL-1.2-no-invariants-or-later", "GFDL-1.2-only", "GFDL-1.2-or-later", + "GFDL-1.3-invariants-only", + "GFDL-1.3-invariants-or-later", + "GFDL-1.3-no-invariants-only", + "GFDL-1.3-no-invariants-or-later", "GFDL-1.3-only", "GFDL-1.3-or-later", - "Giftware", "GL2PS", - "Glide", - "Glulxe", - "gnuplot", + "GLWTPL", "GPL-1.0-only", "GPL-1.0-or-later", - "gSOAP-1.3b", - "HaskellReport", + "Giftware", + "Glide", + "Glulxe", + "Graphics-Gems", + "HP-1986", + "HP-1989", "HPND", + "HPND-DEC", + "HPND-Fenneberg-Livingston", + "HPND-INRIA-IMAG", + "HPND-Kevlin-Henney", + "HPND-MIT-disclaimer", + "HPND-Markus-Kuhn", + "HPND-Pbmplus", + "HPND-UC", + "HPND-doc", + "HPND-doc-sell", + "HPND-export-US", + "HPND-export-US-modify", + "HPND-sell-MIT-disclaimer-xserver", + "HPND-sell-regexpr", "HPND-sell-variant", + "HPND-sell-variant-MIT-disclaimer", + "HTMLTIDY", + "HaskellReport", + "Hippocratic-2.1", "IBM-pibs", "ICU", + "IEC-Code-Components-EULA", "IJG", + "IJG-short", + "IPL-1.0", + "ISC-Veillard", "ImageMagick", - "iMatix", "Imlib2", "Info-ZIP", - "Intel-ACPI", + "Inner-Net-2.0", "Intel", + "Intel-ACPI", "Interbase-1.0", - "IPL-1.0", - "JasPer-2.0", + "JPL-image", "JPNIC", "JSON", + "Jam", + "JasPer-2.0", + "Kastrup", + "Kazlib", + "Knuth-CTAN", "LAL-1.2", "LAL-1.3", - "Latex2e", - "Leptonica", "LGPLLR", - "Libpng", - "libpng-2.0", - "libtiff", - "Linux-OpenIB", + "LOOP", + "LPD-document", "LPL-1.0", "LPL-1.02", "LPPL-1.0", @@ -183,41 +292,84 @@ "LPPL-1.2", "LPPL-1.3a", "LPPL-1.3c", - "MakeIndex", + "LZMA-SDK-9.11-to-9.20", + "LZMA-SDK-9.22", + "Latex2e", + "Latex2e-translated-notice", + "Leptonica", + "Libpng", + "Linux-OpenIB", + "Linux-man-pages-1-para", + "Linux-man-pages-copyleft", + "Linux-man-pages-copyleft-2-para", + "Linux-man-pages-copyleft-var", + "Lucida-Bitmap-Fonts", "MIT-0", - "MIT-advertising", "MIT-CMU", + "MIT-Festival", + "MIT-Modern-Variant", + "MIT-Wu", + "MIT-advertising", "MIT-enna", "MIT-feh", + "MIT-open-group", + "MIT-testregex", "MITNFA", - "Motosoto", - "mpich2", + "MMIXware", + "MPEG-SSG", "MPL-1.0", "MPL-1.1", "MPL-2.0-no-copyleft-exception", + "MS-LPL", "MTLL", + "Mackerras-3-Clause", + "Mackerras-3-Clause-acknowledgment", + "MakeIndex", + "Martin-Birgmeier", + "McPhee-slideshow", + "Minpack", + "Motosoto", + "MulanPSL-1.0", + "MulanPSL-2.0", "Multics", "Mup", - "Naumen", + "NAIST-2003", "NBPL-1.0", + "NCGL-UK-2.0", "NCSA", - "Net-SNMP", - "NetCDF", - "Newsletr", "NGPL", + "NICTA-1.0", + "NIST-PD", + "NIST-PD-fallback", + "NIST-Software", "NLOD-1.0", + "NLOD-2.0", "NLPL", - "Nokia", "NOSL", - "Noweb", "NPL-1.0", "NPL-1.1", "NRL", + "NTP-0", + "Naumen", + "Net-SNMP", + "NetCDF", + "Newsletr", + "Nokia", + "Noweb", + "O-UDA-1.0", "OCCT-PL", "OCLC-2.0", - "ODbL-1.0", "ODC-By-1.0", + "ODbL-1.0", + "OFFIS", + "OFL-1.0-RFN", + "OFL-1.0-no-RFN", "OFL-1.1", + "OFL-1.1-RFN", + "OFL-1.1-no-RFN", + "OGC-1.0", + "OGDL-Taiwan-1.0", + "OGL-Canada-2.0", "OGL-UK-1.0", "OGL-UK-2.0", "OGL-UK-3.0", @@ -225,101 +377,185 @@ "OLDAP-1.2", "OLDAP-1.3", "OLDAP-1.4", - "OLDAP-2.0.1", "OLDAP-2.0", + "OLDAP-2.0.1", "OLDAP-2.1", + "OLDAP-2.2", "OLDAP-2.2.1", "OLDAP-2.2.2", - "OLDAP-2.2", "OLDAP-2.3", "OLDAP-2.4", "OLDAP-2.5", "OLDAP-2.6", "OLDAP-2.7", "OLDAP-2.8", + "OLFL-1.3", "OML", - "OpenSSL", "OPL-1.0", + "OPL-UK-3.0", + "OPUBL-1.0", "OSET-PL-2.1", "OSL-1.0", "OSL-1.1", "OSL-2.0", - "Parity-6.0.0", + "OpenPBS-2.3", + "OpenSSL", + "OpenSSL-standalone", + "OpenVision", + "PADL", "PDDL-1.0", "PHP-3.0", "PHP-3.01", + "PSF-2.0", + "Parity-6.0.0", + "Parity-7.0.0", + "Pixar", "Plexus", + "PolyForm-Noncommercial-1.0.0", + "PolyForm-Small-Business-1.0.0", "PostgreSQL", - "psfrag", - "psutils", "Python-2.0", + "Python-2.0.1", + "QPL-1.0-INRIA-2004", "Qhull", - "Rdisc", "RHeCos-1.1", "RPL-1.1", "RPSL-1.0", "RSA-MD", "RSCPL", + "Rdisc", "Ruby", "SAX-PD", - "Saxpath", + "SAX-PD-2.0", "SCEA", - "Sendmail-8.23", - "Sendmail", "SGI-B-1.0", "SGI-B-1.1", "SGI-B-2.0", + "SGI-OpenGL", + "SGP4", "SHL-0.5", "SHL-0.51", - "SISSL-1.2", "SISSL", - "Sleepycat", + "SISSL-1.2", + "SL", "SMLNJ", "SMPPL", "SNIA", + "SPL-1.0", + "SSH-OpenSSH", + "SSH-short", + "SSLeay-standalone", + "SSPL-1.0", + "SWL", + "Saxpath", + "SchemeReport", + "Sendmail", + "Sendmail-8.23", + "Sleepycat", + "Soundex", "Spencer-86", "Spencer-94", "Spencer-99", - "SPL-1.0", - "SSPL-1.0", "SugarCRM-1.1.3", - "SWL", + "Sun-PPP", + "SunPro", + "Symlinks", "TAPR-OHL-1.0", "TCL", "TCP-wrappers", + "TGPPL-1.0", "TMate", "TORQUE-1.1", "TOSL", + "TPDL", + "TPL-1.0", + "TTWL", + "TTYP0", "TU-Berlin-1.0", "TU-Berlin-2.0", + "TermReadKey", + "UCAR", + "UCL-1.0", + "UMich-Merit", + "URT-RLE", + "Unicode-3.0", "Unicode-DFS-2015", "Unicode-DFS-2016", "Unicode-TOU", + "UnixCrypt", "Unlicense", - "Vim", "VOSTROM", "VSL-1.0", + "Vim", + "W3C", "W3C-19980720", "W3C-20150513", - "W3C", + "WTFPL", "Watcom-1.0", + "Widget-Workshop", "Wsuipa", - "WTFPL", "X11", - "Xerox", + "X11-distribute-modifications-variant", "XFree86-1.1", - "xinetd", - "Xnet", - "xpp", "XSkat", + "Xdebug-1.03", + "Xerox", + "Xfig", + "Xnet", "YPL-1.0", "YPL-1.1", + "ZPL-1.1", + "ZPL-2.0", + "ZPL-2.1", "Zed", + "Zeeff", "Zend-2.0", "Zimbra-1.3", "Zimbra-1.4", - "zlib-acknowledgement", - "ZPL-1.1", - "ZPL-2.0", - "ZPL-2.1" + "bcrypt-Solar-Designer", + "blessing", + "bzip2-1.0.6", + "check-cvs", + "checkmk", + "copyleft-next-0.3.0", + "copyleft-next-0.3.1", + "curl", + "diffmark", + "dtoa", + "dvipdfm", + "eGenix", + "etalab-2.0", + "fwlw", + "gSOAP-1.3b", + "gnuplot", + "gtkbook", + "hdparm", + "iMatix", + "libpng-2.0", + "libselinux-1.0", + "libtiff", + "libutil-David-Nugent", + "lsof", + "magaz", + "mailprio", + "metamail", + "mpi-permissive", + "mpich2", + "mplus", + "pnmstitch", + "psfrag", + "psutils", + "python-ldap", + "radvd", + "snprintf", + "softSurfer", + "ssh-keyscan", + "swrule", + "ulem", + "w3m", + "xinetd", + "xkeyboard-config-Zinoviev", + "xlock", + "xpp", + "zlib-acknowledgement" ] diff --git a/libs/debug.js b/libs/debug.js index 43e9ed0d0..8afbfdffd 100644 --- a/libs/debug.js +++ b/libs/debug.js @@ -1,15 +1,27 @@ 'use strict'; +var inspector = require('inspector'); var fs = require('fs'); +var os = require('os'); + +var git = require('git-rev-sync'); + +var pkg = require('../package.json'); +pkg.org = pkg.name.substring(0, pkg.name.indexOf('.')); var isPro = process.env.NODE_ENV === 'production'; var isDev = !isPro; -var isDbg = typeof v8debug === 'object'; +var isDbg = typeof v8debug === 'object' || inspector.url(); -var isSecure = null; var privkey = null; var fullchain = null; var chain = null; +var isSecured = null; +var isRenewable = null; + +var uaOUJS = null; +var hash = null; + try { // Check for primary keys @@ -25,6 +37,7 @@ try { exports.fullchain = fullchain; exports.chain = chain; exports.isSecured = true; + exports.isRenewable = false; } catch (aE) { // Check for backup alternate keys @@ -41,6 +54,7 @@ try { exports.fullchain = fullchain; exports.chain = chain; exports.isSecured = true; + exports.isRenewable = !!process.env.ATTEMPT_RENEWAL; } catch (aE) { // Ensure that all items are nulled or equivalent @@ -48,6 +62,7 @@ try { exports.fullchain = null; exports.chain = null; exports.isSecured = false; + exports.isRenewable = !!process.env.ATTEMPT_RENEWAL; } } @@ -55,6 +70,19 @@ exports.isPro = isPro; exports.isDev = isDev; exports.isDbg = isDbg; + +hash = git.short(); // NOTE: Synchronous +uaOUJS = pkg.org + '/' + pkg.version + + ' (' + os.type() + '; ' + os.arch() + '; rv:' + hash + ') ' + + 'OUJS/20131106 ' + pkg.name + '/' + hash; +exports.uaOUJS = uaOUJS; + +// NOTE: Requires DB migration for changing below settings +exports.rLogographic = /[\p{sc=Han}\p{sc=Hiragana}\p{sc=Katakana}\p{sc=Hangul}]/u; +exports.logographicDivisor = 4; + +// /NOTE: + // ES6+ in use to eliminate extra property class statusError extends Error { constructor (aStatus, aCode) { diff --git a/libs/flag.js b/libs/flag.js index b509ceca5..81db8f53f 100644 --- a/libs/flag.js +++ b/libs/flag.js @@ -114,7 +114,9 @@ function getThreshold(aModel, aContent, aAuthor, aCallback) { } exports.getThreshold = getThreshold; -function saveContent(aModel, aContent, aAuthor, aFlags, aIsFlagging, aCallback) { +function saveContent(aModel, aContent, aAuthor, aWeight, aIsFlagging, aCallback) { + var weight = (aAuthor.role < 4 ? 2 : 1); + if (!aContent.flags) { aContent.flags = {}; } @@ -127,14 +129,14 @@ function saveContent(aModel, aContent, aAuthor, aFlags, aIsFlagging, aCallback) aContent.flags.absolute = 0; } - aContent.flags.critical += aFlags; + aContent.flags.critical += aWeight; if (aIsFlagging) { aContent.flags.absolute += - (aFlags > 0 ? 1 : (aFlags < 0 && aContent.flags.absolute !== 0 ? -1 : 0)); + (aWeight > 0 ? 1 : (aWeight < 0 && aContent.flags.absolute !== 0 ? -1 : 0)); } - if (aContent.flags.critical >= thresholds[aModel.modelName] * (aAuthor.role < 4 ? 2 : 1)) { + if (aContent.flags.critical >= thresholds[aModel.modelName] * weight) { return getThreshold(aModel, aContent, aAuthor, function (aThreshold) { aContent.flagged = aContent.flags.critical >= aThreshold; @@ -163,11 +165,14 @@ function saveContent(aModel, aContent, aAuthor, aFlags, aIsFlagging, aCallback) exports.saveContent = saveContent; function flag(aModel, aContent, aUser, aAuthor, aReason, aCallback) { + var now = new Date(); var flag = new Flag({ 'model': aModel.modelName, '_contentId': aContent._id, '_userId': aUser._id, - 'reason': aReason + 'weight': aUser.role < 4 ? 2 : 1, + 'reason': aReason, + 'created': now }); flag.save(function (aErr, aFlag) { @@ -189,7 +194,7 @@ function flag(aModel, aContent, aUser, aAuthor, aReason, aCallback) { aContent.flagged = false; } - saveContent(aModel, aContent, aAuthor, aUser.role < 4 ? 2 : 1, true, aCallback); + saveContent(aModel, aContent, aAuthor, aFlag.weight, true, aCallback); }); } @@ -236,7 +241,7 @@ exports.unflag = function (aModel, aContent, aUser, aReason, aCallback) { aFlag.remove(function (aErr) { // WARNING: No err handling - saveContent(aModel, aContent, aAuthor, aUser.role < 4 ? -2 : -1, true, aCallback); + saveContent(aModel, aContent, aAuthor, -aFlag.weight, true, aCallback); }); } diff --git a/libs/githubClient.js b/libs/githubClient.js index 20c058b27..0c94dc414 100644 --- a/libs/githubClient.js +++ b/libs/githubClient.js @@ -4,40 +4,87 @@ var isPro = require('../libs/debug').isPro; var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; +var uaOUJS = require('../libs/debug').uaOUJS; // -var GitHubApi = require("github"); var _ = require("underscore"); var async = require('async'); var util = require('util'); var request = require('request'); var colors = require('ansi-colors'); +var GitHubApi = require("github"); +var createOAuthAppAuth = require("@octokit/auth-oauth-app").createOAuthAppAuth; + // Client var github = new GitHubApi({ - version: "3.0.0" + version: "3.0.0", + debug: (isDbg ? true : false), + headers: { + "User-Agent": uaOUJS + (process.env.UA_SECRET ? ' ' + process.env.UA_SECRET : '') + } }); module.exports = github; // Authenticate Client var Strategy = require('../models/strategy').Strategy; -Strategy.findOne({ name: 'github' }, function (aErr, aStrat) { +Strategy.findOne({ name: 'github' }, async function (aErr, aStrat) { + var auth = null; + var appAuthentication = null; + if (aErr) console.error(aErr); - if (aStrat) { + if (aStrat && process.env.DISABLE_SCRIPT_IMPORT !== 'true') { + + // TODO: Incomplete migration here + auth = createOAuthAppAuth({ + clientType: 'oauth-app', + clientId: aStrat.id, + clientSecret: aStrat.key + }); + + appAuthentication = await auth({ + type: "oauth-app" + }); + + // TODO: Do something with `appAuthentication` + + + // DEPRECATED: See #1705 + // NOTE: We are technically an oauth app client but uses the same authentication type + // methodology in the static version of the dependency. In future versions it may be different. github.authenticate({ - type: 'oauth', - key: aStrat.id, - secret: aStrat.key, + type: 'basic', + username: aStrat.id, + password: aStrat.key }); - console.log(colors.green('GitHub client authenticated')); + + // TODO: error handler for UnhandledPromiseRejectionWarning if it crops up after deprecation. + // Forced invalid credentials and no error thrown but doesn't mean that they won't appear. + + if (github.auth) { + console.log(colors.green([ + 'GitHub client (a.k.a this app) DOES contain authentication credentials.', + 'Higher rate limit may be available' + ].join('\n'))); + } + else { + console.log(colors.red([ + 'GitHub client (a.k.a this app) DOES NOT contain authentication credentials.', + 'Critical error with dependency.' + ].join('\n'))); + } } else { - console.warn(colors.yellow('GitHub client NOT authenticated. Will have a lower Rate Limit.')); + console.warn(colors.yellow([ + 'GitHub client (a.k.a this app) DOES NOT contain authentication credentials.', + 'Lower rate limit will be available.' + ].join('\n'))); } }); + // Util functions for the client. github.usercontent = github.usercontent || {}; @@ -67,7 +114,12 @@ var githubUserContentGetBlobAsUtf8 = function (aMsg, aCallback) { async.waterfall([ function (aCallback) { var url = githubUserContentBuildUrl(aMsg.user, aMsg.repo, aMsg.path); - request.get(url, aCallback); + request.get({ + url: url, + headers: { + 'User-Agent': uaOUJS + (process.env.UA_SECRET ? ' ' + process.env.UA_SECRET : '') + } + }, aCallback); }, function (aResponse, aBody, aCallback) { if (aResponse.statusCode !== 200) diff --git a/libs/helpers.js b/libs/helpers.js index f38ec587f..9af9852e2 100644 --- a/libs/helpers.js +++ b/libs/helpers.js @@ -197,6 +197,15 @@ exports.isFQUrl = function (aString, aOptions) { return false; }; +exports.appendUrlLeaf = function (aUrl, aLeaf) { + let target = new URL(aUrl); + + target.pathname = target.pathname.replace(/\/$/, '') + '/' + + aLeaf.replace(/^\//, '').replace(/\/$/); + + return target.href; +} + // Helper function to ensure value is type Integer `number` or `null` // Please be very careful if this is edited exports.ensureIntegerOrNull = function (aEnvVar) { @@ -217,12 +226,12 @@ exports.ensureIntegerOrNull = function (aEnvVar) { exports.port = process.env.PORT || 8080; exports.securePort = process.env.SECURE_PORT || 8081; -exports.baseOrigin = 'https://openuserjs.org'; +exports.baseOrigin = (isPro ? 'https://openuserjs.org' : 'http://localhost:' + exports.port); // NOTE: Watchpoint // Absolute pattern and is combined for pro and dev exports.patternHasSameOrigin = - '(?:https?://openuserjs\.org(?::' + exports.securePort + ')?|http://(?:oujs\.org' + - (isDev ? '|localhost:' + exports.port : '' ) + '))'; + '(?:https?://openuserjs\.org(?::' + exports.securePort + ')?' + + (isDev ? '|http://localhost:' + exports.port + ')' : ')' ); // Possible pattern and is split for pro vs. dev // NOTE: This re is quite sensitive to changes esp. with `|` @@ -253,3 +262,18 @@ exports.isSameOrigin = function (aUrl) { URL: url }; } + +exports.getRedirect = function (aReq) { + var referer = aReq.get('Referer'); + var redirect = '/'; + + if (referer) { + referer = url.parse(referer); // NOTE: Legacy + if (referer.hostname === aReq.hostname) { + redirect = referer.path; + } + } + + return redirect; +} + diff --git a/libs/markdown.js b/libs/markdown.js index 0871c9ec0..5b5799c80 100644 --- a/libs/markdown.js +++ b/libs/markdown.js @@ -7,19 +7,83 @@ var isDbg = require('../libs/debug').isDbg; // var _ = require('underscore'); -var marked = require('marked'); +var { Marked } = require('marked'); +var { markedHighlight } = require('marked-highlight'); var hljs = require('highlight.js'); var sanitizeHtml = require('sanitize-html'); var colors = require('ansi-colors'); var isSameOrigin = require('./helpers').isSameOrigin; -var jsdom = require("jsdom"); -var { JSDOM } = jsdom; +var { JSDOM } = require("jsdom"); var htmlWhitelistPost = require('./htmlWhitelistPost.json'); var htmlWhitelistFollow = require('./htmlWhitelistFollow.json'); + +function highlighter(aCode, aLang) { + var obj = null; + var langs = [ // NOTE: More likely to less likely + 'javascript', 'xpath', 'xml', + 'css', 'less', 'scss', + 'json', + 'diff', + 'shell', 'console', + 'bash', 'dos', + 'vbscript' + ]; + + if (aLang && hljs.getLanguage(aLang)) { + try { + return hljs.highlight(aCode, { language: aLang }).value; + } catch (aE) { + if (isDev) { + console.error([ + colors.red('Dependency named highlighting failed with:'), + aE + + ].join('\n')); + } + } + } + + try { + obj = hljs.highlightAuto(aCode); + + if (langs.indexOf(obj.language) > -1) { + return obj.value; + } else { + if (isDev) { + console.log([ + colors.yellow('Unusual auto-detected md language code is') + + '`' + colors.cyan(obj.language) + '`', + + ].join('\n')); + } + return hljs.highlightAuto(aCode, langs).value; + } + } catch (aE) { + if (isDev) { + console.error([ + colors.red('Dependency automatic named highlighting failed with:'), + aE + + ].join('\n')); + } + } + + // If any external package failure don't block return e.g. prevent empty + return aCode; +}; + +var marked = new Marked( + markedHighlight({ + langPrefix: 'hljs language-', + highlight: highlighter + }) +); + var renderer = new marked.Renderer(); + var blockRenderers = [ 'blockquote', 'html', @@ -28,6 +92,8 @@ var blockRenderers = [ 'table' ]; + + // Transform exact Github Flavored Markdown generated style tags to bootstrap custom classes // to allow the sanitizer to whitelist on th and td tags for table alignment function gfmStyleToBootstrapClass(aTagName, aAttribs) { @@ -62,8 +128,12 @@ function externalPolicy(aTagName, aAttribs) { var attribRelAdd = []; var attribRelReject = [ 'dns-prefetch', + 'modulepreload', + 'pingback', 'preconnect', - 'prefetch' + 'prefetch', + 'preload', + 'prerender' ]; var obj = null; var dn = null; @@ -76,6 +146,7 @@ function externalPolicy(aTagName, aAttribs) { attribRelAdd.push('external'); attribRelAdd.push('noreferrer'); attribRelAdd.push('noopener'); + attribRelAdd.push('ugc'); if (obj.URL) { matches = obj.URL.hostname.match(/\.?(.*?\..*)$/); @@ -138,11 +209,10 @@ function sanitize(aHtml) { // Sanitize the output from the block level renderers blockRenderers.forEach(function (aType) { renderer[aType] = function () { - // Sanitize first to close any tags + // Render Markdown type first then sanitize HTML including any closing of tags var sanitized = sanitize(marked.Renderer.prototype[aType].apply(renderer, arguments)); - // Autolink most usernames - + // Autolink most usernames. var dom = new JSDOM('
'); var win = dom.window; var doc = win.document; @@ -158,6 +228,8 @@ blockRenderers.forEach(function (aType) { var htmlContainer = null; var thisNode = null; + var matches = null; + hookNode.innerHTML = sanitized; xpr = doc.evaluate( @@ -193,7 +265,17 @@ blockRenderers.forEach(function (aType) { } } - sanitized = hookNode.innerHTML + sanitized = hookNode.innerHTML; + } + + // Workaround for #1775 + if (aType === 'html') { + matches = arguments[0].match(/^<(\/?)([a-z]+)(?![^>]*\/>)[^>]*>$/i); + if (matches && matches[2] && sanitize('<' + matches[2] + '>')) { + sanitized = matches[1] + ? '' + : sanitized.replace(new RegExp('<\/' + matches[2].toLowerCase() + '>$'), ''); + } } return sanitized; @@ -223,70 +305,13 @@ renderer.link = function (aHref, aTitle, aText) { // Set the options to use for rendering markdown // Keep in sync with ./views/includes/scripts/markdownEditor.html marked.setOptions({ - highlight: function (aCode, aLang) { - var obj = null; - var lang = [ // NOTE: More likely to less likely - 'javascript', 'xpath', 'xml', - 'css', 'less', 'scss', - 'json', - 'diff', - 'shell', - 'bash', 'dos', - 'vbscript' - ]; - - if (aLang && hljs.getLanguage(aLang)) { - try { - return hljs.highlight(aLang, aCode).value; - } catch (aE) { - if (isDev) { - console.error([ - colors.red('Dependency named highlighting failed with:'), - aE - - ].join('\n')); - } - } - } - - try { - obj = hljs.highlightAuto(aCode); - - if (lang.indexOf(obj.language) > -1) { - return obj.value; - } else { - if (isDev) { - console.log([ - colors.yellow('Unusual auto-detected md language code is') - + '`' + colors.cyan(obj.language) + '`', - - ].join('\n')); - } - return hljs.highlightAuto(aCode, lang).value; - } - } catch (aE) { - if (isDev) { - console.error([ - colors.red('Dependency automatic named highlighting failed with:'), - aE - - ].join('\n')); - } - } - - // If any external package failure don't block return e.g. prevent empty - return aCode; - }, renderer: renderer, - gfm: true, - tables: true, + async: false, breaks: true, - pedantic: false, - sanitize: false, // we use sanitize-html to sanitize HTML - smartLists: true, - smartypants: false + gfm: true, + pedantic: false }); exports.renderMd = function (aText) { - return marked(aText); + return marked.parse(aText); }; diff --git a/libs/modelParser.js b/libs/modelParser.js index b037835c6..b8757f96e 100644 --- a/libs/modelParser.js +++ b/libs/modelParser.js @@ -4,6 +4,10 @@ var isPro = require('../libs/debug').isPro; var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; +var statusError = require('../libs/debug').statusError; + +var rLogographic = require('../libs/debug').rLogographic; +var logographicDivisor = require('../libs/debug').logographicDivisor; // @@ -29,6 +33,8 @@ var decode = require('../libs/helpers').decode; var isFQUrl = require('../libs/helpers').isFQUrl; var isSameOrigin = require('../libs/helpers').isSameOrigin; var patternHasSameOrigin = require('../libs/helpers').patternHasSameOrigin; +var scriptStorageLib = require('../libs/scriptStorage').invalidKey; +var settings = require('../models/settings.json'); //--- Configuration inclusions @@ -273,6 +279,8 @@ var parseScript = function (aScript) { var supportURL = null; var contributionURL = null; + var CVES = null; + var updateURL = null; var updateUtf = null; var updateURLForceCheck = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; @@ -287,6 +295,10 @@ var parseScript = function (aScript) { '^' + patternHasSameOrigin + '/(?:meta|install|src/scripts)/(.+?)/(.+?)\.(?:meta|user)\.js$' ); + var rIsolatedLocalMetaUrl = new RegExp( + '^' + patternHasSameOrigin + + '/(?:meta|install|src/scripts)/(.+?)/(.+?)\.(?:meta)\.js$' + ); var rSameOrigin = new RegExp( '^' + patternHasSameOrigin @@ -308,6 +320,9 @@ var parseScript = function (aScript) { var matches = null; + var logographic = null; + var storeDescriptionLength = null; + if (!aScript) { return; } @@ -331,8 +346,14 @@ var parseScript = function (aScript) { } }); - if (script.description && script._description && script.description.length > script._description.length) { - script.hasLongDescription = true; + logographic = rLogographic.test(script.description); + + if (script.description && script._description + && script.description.length && script.description.length > logographicDivisor + && script.description.length > (logographic + ? parseInt(settings.scriptSearchQueryStoreMaxDescription / logographicDivisor) + : settings.scriptSearchQueryStoreMaxDescription)) { + script.hasLongDescription = true; } } @@ -485,16 +506,33 @@ var parseScript = function (aScript) { // Dates + parseDateProperty(script, 'created'); parseDateProperty(script, 'updated'); - parseDateProperty(script, '_since'); // Virtual + + // CVE + CVES = findMeta(script.meta, 'OpenUserJS.CVE'); + if (!!CVES && script.meta['OpenUserJS'].CVE.length > 0) { + script.hasCVE = true; + script.CVES = CVES; + + script.showSourceNotices = true; + } // Hash - script.hashShort = script.hash ? script.hash.substr(0, 7) : 'undefined'; + script.hashShort = 'undefined'; + script.hashSRI = 'undefined'; + + if (script.hash) { + // NOTE: May be absent in dev DB but should not be in pro DB + script.hashShort = script.hash.substr(0, 7); + script.hashSRI = 'sha512-' + Buffer.from(script.hash, 'hex').toString('base64'); + } - if (script._since && script.updated && script._since.toString() !== script.updated.toString()) { + if (script.created && script.updated && script.created.toString() !== script.updated.toString()) { script.isUpdated = true; } + // TODO: Mimic scriptStorageLib.invalidKey return value here?? // Update Url // `@updateURL` must be exact here for OUJS hosted checks with updateURLForceCheck // e.g. no `search`, no `hash` @@ -514,7 +552,7 @@ var parseScript = function (aScript) { } finally { if (!script.hasInvalidUpdateURL) { // Validate `author` and `name` (installNameBase) to this scripts meta only - matches = updateUtf.match(rAnyLocalMetaUrl); + matches = updateUtf.match((updateURLForceCheck ? rIsolatedLocalMetaUrl : rAnyLocalMetaUrl)); if (matches) { if (script.authorSlug.toLowerCase() + '/' + script.nameSlug === matches[1].toLowerCase() + '/' + matches[2]) @@ -579,6 +617,8 @@ var parseScript = function (aScript) { } } + + // Download Url downloadURL = findMeta(script.meta, 'UserScript.downloadURL.0.value'); if (downloadURL) { @@ -644,8 +684,13 @@ var parseUser = function (aUser) { user.isAdmin = user.role < 3; user.isFounder = user.role < 2; user.isRoot = user.role < 1; + + user.isTrusted = user.isMod || user.role === 6; + user.roleName = userRoles[user.role]; + user.showFlags = user.role >= 3; + // user.slug = user.name; @@ -656,6 +701,7 @@ var parseUser = function (aUser) { user.userPageUrl = '/users/' + user.slugUrl; user.userCommentListPageUrl = user.userPageUrl + '/comments'; user.userScriptListPageUrl = user.userPageUrl + '/scripts'; + user.userSyncListPageUrl = user.userPageUrl + '/syncs'; user.userGitHubRepoListPageUrl = user.userPageUrl + '/github/repos'; user.userGitHubRepoPageUrl = user.userPageUrl + '/github/repo'; user.userGitHubImportPageUrl = user.userPageUrl + '/github/import'; @@ -668,6 +714,7 @@ var parseUser = function (aUser) { user.userPageUri = '/users/' + user.slugUri; user.userCommentListPageUri = user.userPageUri + '/comments'; user.userScriptListPageUri = user.userPageUri + '/scripts'; + user.userSyncListPageUri = user.userPageUri + '/syncs'; user.userGitHubRepoListPageUri = user.userPageUri + '/github/repos'; user.userGitHubRepoPageUri = user.userPageUri + '/github/repo'; user.userGitHubImportPageUri = user.userPageUri + '/github/import'; @@ -689,9 +736,20 @@ var parseUser = function (aUser) { // Strategies user.userStrategies = user.strategies; + user.hasGithub = user.strategies && user.strategies.indexOf('github') > -1; // NOTE: Watchpoint + user.canSync = user.hasGithub; // Dates - parseDateProperty(user, '_since'); // Virtual + parseDateProperty(user, 'created'); + parseDateProperty(user, 'updated'); + + if (user.created && user.updated) { + user.isUpdated = true; + } + + if (user.created && user.updated && user.created.toString() === user.updated.toString()) { + user.isUpdatedWarning = true; + } return user; }; @@ -801,8 +859,8 @@ var parseDiscussion = function (aDiscussion) { parseDateProperty(discussion, 'created'); parseDateProperty(discussion, 'updated'); - if (discussion._since && discussion.updated - && discussion._since.toString() !== discussion.updated.toString()) { + if (discussion.created && discussion.updated + && discussion.created.toString() !== discussion.updated.toString()) { discussion.isUpdated = discussion.comments > 1 ? true : false; } @@ -887,7 +945,7 @@ var parseComment = function (aComment) { comment.ua = {}; comment.ua.raw = comment.userAgent; - ua = useragent.parse(comment.userAgent).family.toLowerCase().replace(/\s+/g, '-'); + ua = useragent.parse(comment.userAgent).family.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-'); if (ua !== 'other') { comment.ua.class = 'fa-lg ua-' + ua; } else if (comment.userAgent) { @@ -913,6 +971,31 @@ exports.renderComment = function (aComment) { aComment.contentRendered = renderMd(aComment.content); }; +/** + * Sync + */ + +// +var parseSync = function (aSync) { + var sync = null; + + if (!aSync) { + return; + } + sync = aSync.toObject ? aSync.toObject() : aSync; + + sync.targetUrl = decodeURIComponent(sync.target); + sync.targetUrlBasename = decodeURIComponent(sync.target.split('/').pop()); + + // Dates + parseDateProperty(sync, 'created'); + parseDateProperty(sync, 'updated'); + + return sync; +}; +parseModelFnMap.Sync = parseSync; +exports.parseSync = parseSync; + /** * Category */ @@ -1136,7 +1219,7 @@ var parseSession = function (aSession) { oujsOptions.remoteAddressMask = oujsOptions.remoteAddress; oujsOptions.userAgentFamily = useragent - .parse(oujsOptions.userAgent).family.toLowerCase().replace(/\s+/g, '-'); + .parse(oujsOptions.userAgent).family.toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-'); parseDateProperty(oujsOptions, 'since'); cookie.sameSiteStrict = cookie.sameSite === 'strict'; diff --git a/libs/modelQuery.js b/libs/modelQuery.js index 497c338a2..1c3ce62e3 100644 --- a/libs/modelQuery.js +++ b/libs/modelQuery.js @@ -8,8 +8,14 @@ var isDbg = require('../libs/debug').isDbg; // var _ = require('underscore'); +//-- var getDefaultPagination = require('../libs/templateHelpers').getDefaultPagination; +var lockdown = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; + +var authQuery = JSON.parse(process.env.AUTH_QUERY || '{}'); +var limitQuery = JSON.parse(process.env.LIMIT_QUERY || '{}'); + // Transform a "tri-state" value condition to null for true/false/null stored DB values // See also #701 var findOrDefaultToNull = function (aQuery, aKey, aValue, aDefaultValue) { @@ -111,52 +117,92 @@ var parseModelListSearchQuery = function (aModelListQuery, aQuery, aSearchOption }); }; -var parseScriptSearchQuery = function (aScriptListQuery, aQuery) { - if (process.env.LIMIT_SEARCH_QUERY === 'true' - || process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true') { +var parseScriptSearchQuery = function (aScriptListQuery, aQuery, aLimited) { + if (lockdown) { + parseModelListSearchQuery(aScriptListQuery, aQuery, { + partialWordMatchFields: ['name', 'author'], + fullWordMatchFields: [] + }); + } else if (limitQuery.Script === 'true' || aLimited) { parseModelListSearchQuery(aScriptListQuery, aQuery, { - partialWordMatchFields: ['name', '_description', 'author' ], + partialWordMatchFields: ['name', 'author'], fullWordMatchFields: ['meta.UserScript.include.value', 'meta.UserScript.match.value'] }); } else { parseModelListSearchQuery(aScriptListQuery, aQuery, { - partialWordMatchFields: ['name', '_description', 'author', '_about' ], + partialWordMatchFields: ['name', 'author', '_description', '_about'], fullWordMatchFields: ['meta.UserScript.include.value', 'meta.UserScript.match.value'] }); } }; exports.parseScriptSearchQuery = parseScriptSearchQuery; -var parseGroupSearchQuery = function (aGroupListQuery, aQuery) { - parseModelListSearchQuery(aGroupListQuery, aQuery, { - partialWordMatchFields: ['name'], - fullWordMatchFields: [] - }); +var parseGroupSearchQuery = function (aGroupListQuery, aQuery, aLimited) { + if (lockdown || limitQuery.Group === 'true' || aLimited) { + parseModelListSearchQuery(aGroupListQuery, aQuery, { + partialWordMatchFields: ['name'], + fullWordMatchFields: [] + }); + } else { + parseModelListSearchQuery(aGroupListQuery, aQuery, { + partialWordMatchFields: ['name'], + fullWordMatchFields: [] + }); + } }; exports.parseGroupSearchQuery = parseGroupSearchQuery; -var parseDiscussionSearchQuery = function (aDiscussionListQuery, aQuery) { - parseModelListSearchQuery(aDiscussionListQuery, aQuery, { - partialWordMatchFields: ['topic'], - fullWordMatchFields: ['author'] - }); +var parseDiscussionSearchQuery = function (aDiscussionListQuery, aQuery, aLimited) { + if (lockdown || limitQuery.Discussion === 'true' || aLimited) { + parseModelListSearchQuery(aDiscussionListQuery, aQuery, { + partialWordMatchFields: ['topic'], + fullWordMatchFields: ['author'] + }); + } else { + parseModelListSearchQuery(aDiscussionListQuery, aQuery, { + partialWordMatchFields: ['topic'], + fullWordMatchFields: ['author'] + }); + } }; exports.parseDiscussionSearchQuery = parseDiscussionSearchQuery; -var parseCommentSearchQuery = function (aCommentListQuery, aQuery) { - parseModelListSearchQuery(aCommentListQuery, aQuery, { - partialWordMatchFields: ['content'], - fullWordMatchFields: ['author'] - }); +var parseCommentSearchQuery = function (aCommentListQuery, aQuery, aLimited) { + if (lockdown || limitQuery.Comment === 'true' || aLimited) { + parseModelListSearchQuery(aCommentListQuery, aQuery, { + partialWordMatchFields: ['content'], + fullWordMatchFields: ['author'] + }); + } else { + parseModelListSearchQuery(aCommentListQuery, aQuery, { + partialWordMatchFields: ['content'], + fullWordMatchFields: ['author'] + }); + } }; exports.parseCommentSearchQuery = parseCommentSearchQuery; -var parseUserSearchQuery = function (aUserListQuery, aQuery) { - parseModelListSearchQuery(aUserListQuery, aQuery, { - partialWordMatchFields: ['name'], - fullWordMatchFields: [] +var parseSyncSearchQuery = function (aSyncListQuery, aQuery) { + parseModelListSearchQuery(aSyncListQuery, aQuery, { + partialWordMatchFields: [], + fullWordMatchFields: ['target'] }); }; +exports.parseSyncSearchQuery = parseSyncSearchQuery; + +var parseUserSearchQuery = function (aUserListQuery, aQuery, aLimited) { + if (lockdown || limitQuery.User === 'true' || aLimited) { + parseModelListSearchQuery(aUserListQuery, aQuery, { + partialWordMatchFields: ['name'], + fullWordMatchFields: [] + }); + } else { + parseModelListSearchQuery(aUserListQuery, aQuery, { + partialWordMatchFields: ['name'], + fullWordMatchFields: [] + }); + } +}; exports.parseUserSearchQuery = parseUserSearchQuery; var parseRemovedItemSearchQuery = function (aRemovedItemListQuery, aQuery) { @@ -249,17 +295,27 @@ exports.applyModelListQueryFlaggedFilter = applyModelListQueryFlaggedFilter; var applyModelListQueryDefaults = function (aModelListQuery, aOptions, aReq, aDefaultOptions) { var orders = null; + var authedUser = aReq.session.user; + aOptions.authRequired = 'Sign In to '; // Search - if (aReq.query.q) { - aOptions.searchBarValue = aReq.query.q; + if (authedUser || !aOptions.authToSearch) { + aOptions.authRequired = ''; + aOptions.authToSearch = false; - if (aDefaultOptions.parseSearchQueryFn) { - aDefaultOptions.parseSearchQueryFn(aModelListQuery, aReq.query.q); + if (aReq.query.q) { + aOptions.searchBarValue = aReq.query.q; + + if (aDefaultOptions.parseSearchQueryFn) { + aDefaultOptions.parseSearchQueryFn(aModelListQuery, aReq.query.q, !!!authedUser); + } } + } else if (!aReq.query.q) { + aOptions.authToSearch = false; } aOptions.searchBarFormAction = aDefaultOptions.searchBarFormAction || ''; - aOptions.searchBarPlaceholder = aDefaultOptions.searchBarPlaceholder || 'Search'; + aOptions.searchBarPlaceholder = aOptions.authRequired + + aDefaultOptions.searchBarPlaceholder || 'Search'; aOptions.searchBarFormHiddenVariables = aDefaultOptions.searchBarFormHiddenVariables || []; // flagged @@ -311,7 +367,13 @@ var applyModelListQueryDefaults = function (aModelListQuery, aOptions, aReq, aDe break; case 'size': aOptions.orderedBySize = true; - // fallthrough + break; + case 'target': + aOptions.orderedByTarget = true; + break; + case 'response': + aOptions.orderedByResponse = true; + // fallsthrough } }); @@ -322,6 +384,8 @@ var applyModelListQueryDefaults = function (aModelListQuery, aOptions, aReq, aDe }; exports.applyCommentListQueryDefaults = function (aCommentListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.Comment === 'true'; + applyModelListQueryDefaults(aCommentListQuery, aOptions, aReq, { defaultSort: 'created', parseSearchQueryFn: parseCommentSearchQuery, @@ -337,9 +401,20 @@ exports.applyCommentListQueryDefaults = function (aCommentListQuery, aOptions, a }); }; +exports.applySyncListQueryDefaults = function (aSyncListQuery, aOptions, aReq) { + applyModelListQueryDefaults(aSyncListQuery, aOptions, aReq, { + defaultSort: '-created', + parseSearchQueryFn: parseSyncSearchQuery, + searchBarPlaceholder: 'Search Syncs', + filterFlaggedItems: false + }); +}; + exports.applyDiscussionListQueryDefaults = function (aDiscussionListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.Discussion === 'true'; + applyModelListQueryDefaults(aDiscussionListQuery, aOptions, aReq, { - defaultSort: '-updated -rating', + defaultSort: '-updated -created', parseSearchQueryFn: parseDiscussionSearchQuery, searchBarPlaceholder: 'Search Topics', filterFlaggedItems: true @@ -347,6 +422,8 @@ exports.applyDiscussionListQueryDefaults = function (aDiscussionListQuery, aOpti }; exports.applyGroupListQueryDefaults = function (aGroupListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.Group === 'true'; + applyModelListQueryDefaults(aGroupListQuery, aOptions, aReq, { defaultSort: '-rating name', parseSearchQueryFn: parseGroupSearchQuery, @@ -364,6 +441,8 @@ var scriptListQueryDefaults = { }; exports.scriptListQueryDefaults = scriptListQueryDefaults; exports.applyScriptListQueryDefaults = function (aScriptListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.Script === 'true'; + applyModelListQueryDefaults(aScriptListQuery, aOptions, aReq, scriptListQueryDefaults); }; @@ -379,10 +458,14 @@ var libraryListQueryDefaults = { }; exports.libraryListQueryDefaults = libraryListQueryDefaults; exports.applyLibraryListQueryDefaults = function (aLibraryListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.Script === 'true'; + applyModelListQueryDefaults(aLibraryListQuery, aOptions, aReq, libraryListQueryDefaults); }; exports.applyUserListQueryDefaults = function (aUserListQuery, aOptions, aReq) { + aOptions.authToSearch = lockdown || authQuery.User === 'true'; + applyModelListQueryDefaults(aUserListQuery, aOptions, aReq, { defaultSort: 'name', parseSearchQueryFn: parseUserSearchQuery, @@ -433,6 +516,18 @@ exports.applyRemovedItemCommentListQueryDefaults = function (aRemovedItemComment applyModelListQueryDefaults(aRemovedItemCommentListQuery, aOptions, aReq, removedItemCommentListQueryDefaults); }; +var syncListQueryDefaults = { + defaultSort: '-created', + parseSearchQueryFn: parseSyncSearchQuery, + searchBarPlaceholder: 'Search Syncs', + searchBarFormAction: '/', + filterFlaggedItems: false +}; +exports.syncListQueryDefaults = syncListQueryDefaults; +exports.applySyncListQueryDefaults = function (aSyncListQuery, aOptions, aReq) { + applyModelListQueryDefaults(aSyncListQuery, aOptions, aReq, syncListQueryDefaults); +}; + var removedItemDiscussionListQueryDefaults = { defaultSort: '-removed', parseSearchQueryFn: parseRemovedItemSearchQuery, diff --git a/libs/modifySessions.js b/libs/modifySessions.js index ac065c2cb..a68773135 100644 --- a/libs/modifySessions.js +++ b/libs/modifySessions.js @@ -172,32 +172,46 @@ exports.update = function (aReq, aUser, aCallback) { }; // Destroy one session for a user -exports.destroyOne = function (aReq, aUser, aId, aCallback) { +exports.destroyOne = function (aReq, aSkipUserCheck, aUser, aId, aCallback) { var store = aReq.sessionStore; var authedUser = aReq.session.user; - if (!aUser || !aId) { - aCallback('No user or id', null); + if (!aId) { + aCallback('No id to delete', null); + return; + } + + if (!aUser && !aSkipUserCheck) { + aCallback('No user to delete', null); return; } store.get(aId, function (aErr, aSess) { if (aErr || !aSess) { - aCallback('No session', null); + aCallback('No session to delete', null); return; } // We want to know who deleted someone else! // If we didn't want this then this call to get the session // from id would not be necessary. - if (authedUser.name !== aUser.name) { + if (!aUser && aSkipUserCheck) { console.log( '`' + authedUser.name + '`', - 'removed a session by', + 'removed a session id of', + '`' + aId + '`', 'for `' + aSess.username + '`', + (aSess.passport && aSess.passport.oujsOptions && aSess.passport.oujsOptions.authFrom + ? 'authed from `' + aSess.passport.oujsOptions.authFrom + '`' + : '') + ); + } else if (authedUser.name !== aUser.name) { + console.log( + '`' + authedUser.name + '`', + 'removed a session for', '`' + aUser.name + '`', - aSess.passport.oujsOptions.authFrom + (aSess.passport && aSess.passport.oujsOptions && aSess.passport.oujsOptions.authFrom ? 'authed from `' + aSess.passport.oujsOptions.authFrom + '`' - : '' + : '') ); } @@ -249,7 +263,7 @@ exports.getSessionDataList = function (aReq, aOptions, aCallback) { var oujsOptions = session.passport.oujsOptions; session.showExtend = aReq.sessionID === oujsOptions.sid; - session.canExtend = !oujsOptions.extended; + session.canExtend = !oujsOptions.extended && !authedUser._probationary; session.canDestroyOne = true; // TODO: Perhaps do some further conditionals oujsOptions.remoteAddressMask = session.name === authedUser.name && !oujsOptions.authFrom @@ -277,8 +291,8 @@ exports.getSessionDataList = function (aReq, aOptions, aCallback) { ); }; -exports.findSessionData = function (aQuery, aStore, aOptions, aCallback) { - var sessionColl = aStore.db.collection('sessions'); +exports.findSessionData = async function (aQuery, aStore, aOptions, aCallback) { + var sessionColl = await aStore.collectionP; sessionColl.find({ }, function (aErr, aUserSessions) { @@ -317,8 +331,21 @@ exports.findSessionData = function (aQuery, aStore, aOptions, aCallback) { data.passport.oujsOptions.username = data.username || findMeta(data.user, 'name'); + data.passport.oujsOptions.userrole = data.passport.oujsOptions.username + ? data.userrole + : '\u2026'; + data.passport.oujsOptions.newUser = data.newUser; data.passport.oujsOptions.sid = aSessionData._id; + // NOTE: These only shows up during authentication otherwise don't use + if (data.userauth) { + data.passport.oujsOptions.strategy = data.userauth; + } + + if (data.useragent) { + data.passport.oujsOptions.userAgent = data.useragent; + } + // Very simple query filter search check to start. // Currently only looking in `data.passport.oujsOptions.username`. if (aQuery && aQuery.username) { diff --git a/libs/muExpress.js b/libs/muExpress.js index c2d974ed4..76e2e4283 100644 --- a/libs/muExpress.js +++ b/libs/muExpress.js @@ -29,9 +29,28 @@ function renderFile(aRes, aPath, aOptions) { // } if (aRes.oujsOptions) { - aOptions.DNT = aRes.oujsOptions.DNT; aOptions.hideReminderGDPR = aRes.oujsOptions.hideReminderGDPR; aOptions.showReminderListLimit = aRes.oujsOptions.showReminderListLimit; + aOptions.showReminderInstallLimit = aRes.oujsOptions.showReminderInstallLimit; + + // NOTE: Keep in sync with app.js, user.js, and headerReminders.html + aOptions.showInvalidAuth = aRes.oujsOptions.showInvalidAuth; + aOptions.showStratFail = aRes.oujsOptions.showStratFail; + aOptions.showNoConsent = aRes.oujsOptions.showNoConsent; + aOptions.showNoName = aRes.oujsOptions.showNoName; + aOptions.showTooLong = aRes.oujsOptions.showTooLong; + aOptions.showUsernameFail = aRes.oujsOptions.showUsernameFail; + aOptions.showROAuth = aRes.oujsOptions.showROAuth; + aOptions.showRetryAuth = aRes.oujsOptions.showRetryAuth; + aOptions.showAuthFail = aRes.oujsOptions.showAuthFail; + + // NOTE: Keep in sync with app.js, admin.js, user.js, and headerReminders.html + aOptions.showSesssionNoExtend = aRes.oujsOptions.showSesssionNoExtend; + aOptions.showSessionMissingUsername = aRes.oujsOptions.showSessionMissingUsername; + aOptions.showSesssionCurrentSessionProhibited = aRes.oujsOptions.showSesssionCurrentSessionProhibited; + aOptions.showSesssionHigherRankProhibited = aRes.oujsOptions.showSesssionHigherRankProhibited; + aOptions.showSesssionNoOwned = aRes.oujsOptions.showSesssionNoOwned; + aOptions.showSesssionNoAdmin = aRes.oujsOptions.showSesssionNoAdmin; } aRes.set('Content-Type', 'text/html; charset=UTF-8'); diff --git a/libs/passportLoader.js b/libs/passportLoader.js index a4367ea08..52bfbbe3e 100644 --- a/libs/passportLoader.js +++ b/libs/passportLoader.js @@ -7,6 +7,7 @@ var isDbg = require('../libs/debug').isDbg; // var passport = require('passport'); +var colors = require('ansi-colors'); var nil = require('../libs/helpers').nil; @@ -22,10 +23,20 @@ exports.strategyInstances = nil(); // Notice it is general so it can load any passport strategy exports.loadPassport = function (aStrategy) { var requireStr = 'passport-' + aStrategy.name - + (aStrategy.name === 'google' ? '-oauth20' : (aStrategy.name === 'gitlab' ? '2' : '')); - var PassportStrategy = require(requireStr).Strategy; + + (aStrategy.name === 'google' ? '-oauth20' + : (aStrategy.name === 'gitlab' ? '2' + : (aStrategy.name === 'reddit' ? '-commonjs' : ''))); var instance = null; - var authParams = null; + var PassportStrategy = null; + + try { + PassportStrategy = require(requireStr).Strategy; + } catch (aE) { + console.error( + colors.red('Error loading *' + requireStr + '* for stored Auth Strategy API Key') + ); + return; + } if (aStrategy.openid) { instance = new PassportStrategy( diff --git a/libs/passportVerify.js b/libs/passportVerify.js index 28fce8a04..ac0de974d 100644 --- a/libs/passportVerify.js +++ b/libs/passportVerify.js @@ -49,6 +49,7 @@ exports.verify = function (aId, aStrategy, aUsername, aLoggedIn, aDone) { if (!aUser) { User.findOne({ 'name': aUsername }, function (aErr, aUser) { + var now = new Date(); // WARNING: No err handling if (aUser && aLoggedIn) { @@ -78,6 +79,8 @@ exports.verify = function (aId, aStrategy, aUsername, aLoggedIn, aDone) { // Create a new user aUser = new User({ 'name': aUsername, + 'created': now, + 'updated': null, 'auths': [digest], 'strategies': [aStrategy], 'role': userRoles.length - 2, // NOTE: Last array element value is system Reserved diff --git a/libs/repoManager.js b/libs/repoManager.js index 2b9d0df3c..ff0fc4c5c 100644 --- a/libs/repoManager.js +++ b/libs/repoManager.js @@ -4,6 +4,18 @@ var isPro = require('../libs/debug').isPro; var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; +var uaOUJS = require('../libs/debug').uaOUJS; +var statusError = require('../libs/debug').statusError; + +//--- Dependency inclusions +var util = require('util'); +var colors = require('ansi-colors'); + +//--- Model inclusions +var Sync = require('../models/sync').Sync; + +//--- Controller inclusions +var scriptStorage = require('../controllers/scriptStorage'); // var https = require('https'); @@ -21,24 +33,43 @@ var clientId = null; var clientKey = null; Strategy.findOne({ name: 'github' }, function (aErr, aStrat) { - // WARNING: No err handling + if (aErr) { + console.error( aErr.message ); + process.exit(1); + return; + } - clientId = aStrat.id; - clientKey = aStrat.key; + if (!aStrat) { + console.warn( colors.red([ + 'Default GitHub Strategy document not found in DB.', + 'Lower rate limit will be available.' + ].join('\n'))); + } else { + clientId = aStrat.id; + clientKey = aStrat.key; + } }); // Requests a GitHub url and returns the chunks as buffers -function fetchRaw(aHost, aPath, aCallback) { +function fetchRaw(aHost, aPath, aCallback, aOptions) { var options = { hostname: aHost, port: 443, path: aPath, method: 'GET', headers: { - 'User-Agent': 'Node.js' + 'User-Agent': uaOUJS + (process.env.UA_SECRET ? ' ' + process.env.UA_SECRET : '') } }; + if (aOptions) { + // Ideally do a deep merge of aOptions -> options + // But for now, we just need the headers + if (aOptions.headers) { + Object.assign(options.headers, aOptions.headers); + } + } + var req = https.request(options, function (aRes) { @@ -66,10 +97,23 @@ function fetchRaw(aHost, aPath, aCallback) { // Use for call the GitHub JSON api // Returns the JSON parsed object function fetchJSON(aPath, aCallback) { - aPath += '?client_id=' + clientId + '&client_secret=' + clientKey; + var encodedAuth = null; + var opts = null; + + // The old authentication method, which GitHub deprecated + //aPath += '?client_id=' + clientId + '&client_secret=' + clientKey; + // We must now use OAuth Basic (user+key) or Bearer (token) + if (clientId && clientKey) { + encodedAuth = Buffer.from(`${clientId}:${clientKey}`).toString('base64'); + opts = { + headers: { + Authorization: `Basic ${encodedAuth}` + } + }; + } fetchRaw('api.github.com', aPath, function (aBufs) { aCallback(JSON.parse(Buffer.concat(aBufs).toString())); - }); + }, opts); } // This manages actions on the repos of a user @@ -111,29 +155,150 @@ RepoManager.prototype.fetchRecentRepos = function (aCallback) { ], aCallback); }; -// Import scripts on GitHub +// Import scripts to be sync'd into Sync model +RepoManager.prototype.loadSyncs = function (aUpdate, aCallback) { + var arrayOfRepos = this.makeRepoArray(); + var that = this; + + // TODO: Alter usage of makeRepoArray since it causes redundant looping + arrayOfRepos.forEach(function (aRepo) { + async.each(aRepo.scripts, function (aScript, aInnerCallback) { + var hostname = 'raw.githubusercontent.com'; + var uri = '/' + aRepo.user + '/' + aRepo.repo + + '/master' + aScript.path; + + Sync.findOne( + { _authorId: that.user.id, id: aUpdate, target: 'https://' + hostname + uri }, + function (aErr, aSync) { + if (aErr) { + console.error('Error retrieving sync status'); + aInnerCallback(aErr, aSync); + return; + } + + if (aSync) { + // TODO: Maybe update the updated, response, and message to reflect redelivery? + + aInnerCallback(null, aSync); + } else { + var sync = new Sync({ + strat: 'github', + id: aUpdate, + target: 'https://' + hostname + uri, + response: 202, + message: 'Accepted', + created: new Date(), + _authorId: that.user.id + }); + + sync.save(function (aErr, aSync) { + if (aErr || !aSync) { + console.error('Unable to create Sync record'); + aInnerCallback(aErr, aSync); + return; + } + + aInnerCallback(null, aSync); + }); + } + + }); + + }, aCallback); + }); +}; + +// Import scripts from GitHub RepoManager.prototype.loadScripts = function (aUpdate, aCallback) { - var scriptStorage = require('../controllers/scriptStorage'); var arrayOfRepos = this.makeRepoArray(); var that = this; - // TODO: remove usage of makeRepoArray since it causes redundant looping + // TODO: Alter usage of makeRepoArray since it causes redundant looping arrayOfRepos.forEach(function (aRepo) { async.each(aRepo.scripts, function (aScript, aInnerCallback) { + var hostname = 'raw.githubusercontent.com'; + var uri = '/' + aRepo.user + '/' + aRepo.repo + + '/master' + aScript.path; var url = '/' + encodeURI(aRepo.user) + '/' + encodeURI(aRepo.repo) + '/master' + aScript.path; - fetchRaw('raw.githubusercontent.com', url, function (aBufs) { + + fetchRaw(hostname, url, function (aBufs) { + var msg = null; var thisBuf = Buffer.concat(aBufs); if (thisBuf.byteLength <= settings.maximum_upload_script_size) { scriptStorage.getMeta(aBufs, function (aMeta) { if (aMeta) { - scriptStorage.storeScript(that.user, aMeta, thisBuf, aUpdate, - aInnerCallback); + scriptStorage.storeScript(that.user, aMeta, thisBuf, !!aUpdate, + function (aErr, aScript) { + if (aErr || !aScript) { + msg = (aErr instanceof statusError ? aErr.status.message : aErr.message) + || 'Unknown error with storing script'; + Sync.findOneAndUpdate( + { _authorId: that.user.id, id: aUpdate, target: 'https://' + hostname + uri }, { + response: (aErr instanceof statusError ? aErr.status.code : aErr.code), + message: msg, + updated: new Date() + }, + function (aErr, aSync) { + if (aErr || !aSync) { + console.error('Error changing sync status with ' + msg); + return; + } + }); + } else { + msg = 'OK'; + Sync.findOneAndUpdate( + { _authorId: that.user.id, id: aUpdate, target: 'https://' + hostname + uri }, + { response: 200, message: msg, updated: new Date()}, + function (aErr, aSync) { + if (aErr || !aSync) { + console.error('Error changing sync status with ' + msg); + return; + } + }); + } + + aInnerCallback(aErr, aScript); + }); + } else { + msg = 'Metadata block(s) missing.' + Sync.findOneAndUpdate( + { _authorId: that.user.id, id: aUpdate, target: 'https://' + hostname + uri }, + { response: 400, message: msg, updated: new Date()}, + function (aErr, aSync) { + if (aErr || !aSync) { + console.error('Error changing sync status with ' + msg); + return; + } + }); + + aInnerCallback(new statusError({ + message: msg, + code: 400 + }, null)); } }); + } else { + msg = util.format('File size is larger than maximum (%s bytes).', + settings.maximum_upload_script_size); + Sync.findOneAndUpdate( + { _authorId: that.user.id, id: aUpdate, target: 'https://' + hostname + uri }, + { response: 400, message: msg}, + function (aErr, aSync) { + if (aErr || !aSync) { + console.error('Error changing sync status with ' + msg); + return; + } + }); + + aInnerCallback(new statusError({ + message: msg, + code: 400 + }, null)); } }); + }, aCallback); }); }; diff --git a/libs/scriptStorage.js b/libs/scriptStorage.js new file mode 100644 index 000000000..102534b8e --- /dev/null +++ b/libs/scriptStorage.js @@ -0,0 +1,388 @@ +'use strict'; + +// Define some pseudo module globals +var isPro = require('../libs/debug').isPro; +var isDev = require('../libs/debug').isDev; +var isDbg = require('../libs/debug').isDbg; +var statusError = require('../libs/debug').statusError; + +//--- Library inclusions +var cleanFilename = require('../libs/helpers').cleanFilename; +var patternHasSameOrigin = require('../libs/helpers').patternHasSameOrigin; +var isFQUrl = require('../libs/helpers').isFQUrl; + +// + +// Determine validity of a Key +function invalidKey(aAuthorName, aScriptName, aIsLib, aKeyName, aKeyValue) { // TODO: Simplify signature maybe + var keyValue = null; + var keyValueUtf = null; + var matches = null; + + var rSameOrigin = new RegExp( + '^' + patternHasSameOrigin + '$' + ); + var rAnyLocalMetaUrl = new RegExp( + '^' + patternHasSameOrigin + + '/(?:meta|install|src/scripts)/(.+?)/(.+?)\.(?:meta|user)\.js$' + ); + var rIsolatedLocalMetaUrl = new RegExp( + '^' + patternHasSameOrigin + + '/(?:meta|install|src/scripts)/(.+?)/(.+?)\.(?:meta)\.js$' + ); + + var rIsolatedLocalInstallUrl = new RegExp( + '^' + patternHasSameOrigin + + '/(?:meta|install|src/scripts)/(.+?)/(.+)\.(?:user)\.js$' + ); + + var missingExcludeAll = true; + + var lockdown = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; + + var hasInvalidKeys = []; + var hasGrantNone = null; + + switch (aKeyName) { + case 'css': // NOTE: We don't collect these yet (ref lost) + case 'inject-into': // NOTE: We don't collect this yet + case 'priority': // NOTE: We don't collect this yet + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'antifeature': + if (aKeyValue) { + aKeyValue.forEach(function (aElement, aIndex, aArray) { + switch (aElement) { + case 'ads': + case 'membership': + case 'miner': + case 'referral-link': + case 'tracking': + // fallsthrough + break; + default: + hasInvalidKeys.push( + new statusError({ + message: '`@' + aKeyName + + '` with value of `' + aElement + '` is not valid or supported.', + code: 400 // Bad request + }) + ); + } + }); + + if (hasInvalidKeys.length > 0) { + // NOTE: return only first error since header limitations may throw if RFC2047'd + // TODO: May expand this later + return hasInvalidKeys[0]; + } + + } + break; + case 'connect': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'downloadURL': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'exclude': + if (aIsLib) { + if (aKeyValue) { + if (aKeyValue.length > 1) { + return new statusError({ + message: '`@' + aKeyName + + '` must only have one key value in a Library.', + code: 400 + }); + } + + aKeyValue.forEach(function (aElement, aIndex, aArray) { + if (aElement === '*') { + missingExcludeAll = false; + } + }); + } + + if (missingExcludeAll) { + return new statusError({ + message: '`@' + aKeyName + + '` missing value of `*` in a Library.', + code: 400 + }); + } + } + break; + case 'grant': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } else { + if (aKeyValue) { + aKeyValue.forEach(function (aElement, aIndex, aArray) { + switch (aElement) { + case 'none': + hasGrantNone = true; + // fallsthrough + case 'GM.*': + case 'GM_addElement': + case 'GM_addStyle': + case 'GM_addValueChangeListener': + case 'GM.addValueChangeListener': + case 'GM_deleteValue': + case 'GM.deleteValue': + case 'GM_download': + case 'GM_getResourceText': + case 'GM_getResourceURL': + case 'GM.getResourceUrl': + case 'GM_getTab': + case 'GM_getTabs': + case 'GM_getValue': + case 'GM.getValue': + case 'GM_listValues': + case 'GM.listValues': + case 'GM_log': + case 'GM_notification': + case 'GM.notification': + case 'GM_openInTab': + case 'GM.openInTab': + case 'GM_registerMenuCommand': + case 'GM.registerMenuCommand': + case 'GM_removeValueChangeListener': + case 'GM.removeValueChangeListener': + case 'GM_saveTab': + case 'GM_setClipboard': + case 'GM.setClipboard': + case 'GM_setValue': + case 'GM.setValue': + case 'GM_unregisterMenuCommand': + case 'GM_xmlhttpRequest': + case 'GM.xmlHttpRequest': + case 'unsafeWindow': + case 'window.close': + case 'window.focus': + case 'window.onurlchange': + if (hasGrantNone && aArray.length > 1) { + hasInvalidKeys.push( + new statusError({ + message: '`@' + aKeyName + + '` value of `' + aElement + '` is not valid with other `@grant`s.', + code: 400 // Bad request + }) + ); + } + break; + default: + hasInvalidKeys.push( + new statusError({ + message: '`@' + aKeyName + + '` with value of `' + aElement + '` is not valid or supported.', + code: 400 // Bad request + }) + ); + } + }); + + if (hasInvalidKeys.length > 0) { + // NOTE: return only first error since header limitations may throw if RFC2047'd + // TODO: May expand this later + return hasInvalidKeys[0]; + } + } + } + break; + case 'include': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'match': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'noframes': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'require': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'resource': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'run-at': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'unwrap': + if (aIsLib) { + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } + break; + case 'updateURL': + if (aIsLib) { + + if (aKeyValue) { + return new statusError({ + message: '`@' + aKeyName + + '` not valid in a Library.', + code: 400 // Bad request + }); + } + } else { + + if (aKeyValue) { + // Check for decoding error + try { + keyValueUtf = decodeURIComponent(aKeyValue); + } catch (aE) { + return new statusError({ + message: '`@' + aKeyName + + '` has invalid encoding.', + code: 400 // Bad request + }); + } + + // Check for Fully Qualified URL + if (!isFQUrl(aKeyValue)) { + return new statusError({ + message: '`@' + aKeyName + + '` is not a fully qualified URL.', + code: 400 // Bad request + }); + } + + // Validate `author` and `name` (installNameBase) to this scripts meta only + // NOTE: value needs to be decoded already since MongoDB and AWS doesn't store that + matches = keyValueUtf.match(rAnyLocalMetaUrl); + if (matches) { + if (/\.min$/.test(matches[2])) { + return new statusError({ + message: '`@' + aKeyName + + '` must not be a minified URL.', + code: 403 // Forbidden + }); + } else if (cleanFilename(aAuthorName, '').toLowerCase() + + '/' + cleanFilename(aScriptName, '') === + matches[1].toLowerCase() + '/' + matches[2]) { + // Same script + if (lockdown && !rIsolatedLocalMetaUrl.exec(keyValueUtf)) { + return new statusError({ + message: '`@' + aKeyName + + '` must be matched to this scripts .meta.js in lockdown.', + code: 403 // Forbidden + }); + } + } else if (lockdown) { + return new statusError({ + message: '`@' + aKeyName + + '` must not be matched to another scripts .meta.js in lockdown.', + code: 403 // Forbidden + }); + } + } else { + keyValue = new URL(aKeyValue); + if (!rSameOrigin.test(keyValue.origin)) { + // Allow offsite checks + // fallsthrough + } else { + return new statusError({ + message: '`@' + aKeyName + + '` has an invalid value.', + code: 400 // Bad request + }); + } + } + } else if (lockdown) { + return new statusError({ + message: '`@' + aKeyName + + '` is required in lockdown.', + code: 403 // Forbidden + }); + } + } + break; + default: + } + + // Default + return null; +} +exports.invalidKey = invalidKey; diff --git a/libs/templateHelpers.js b/libs/templateHelpers.js index 0ae64253e..4238f5ef1 100644 --- a/libs/templateHelpers.js +++ b/libs/templateHelpers.js @@ -154,12 +154,12 @@ function pageMetadata(aOptions, aTitle, aDescription, aKeywords) { 'user script', 'user scripts', 'user.js', + '.user.js', 'repository', 'Greasemonkey', 'Greasemonkey Port', - 'Scriptish', - 'TamperMonkey', - 'Violent monkey', + 'Tampermonkey', + 'Violentmonkey', 'JavaScript', 'add-ons', 'extensions', diff --git a/libs/vote.js b/libs/vote.js index 52f138256..9b4aba076 100644 --- a/libs/vote.js +++ b/libs/vote.js @@ -117,8 +117,10 @@ function saveScript(aScript, aAuthor, aFlags, aCallback) { exports.saveScript = saveScript; function newVote(aScript, aUser, aAuthor, aCasting, aCallback) { + var now = new Date(); var vote = new Vote({ vote: aCasting, + created: now, _scriptId: aScript._id, _userId: aUser._id }); diff --git a/models/comment.js b/models/comment.js index e0f0addc9..15bc7b714 100644 --- a/models/comment.js +++ b/models/comment.js @@ -36,4 +36,20 @@ var commentSchema = new Schema({ var Comment = mongoose.model('Comment', commentSchema); +Comment.syncIndexes(function () { + Comment.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Comment indexes:\n', aIndexes); + }).catch(console.error); +}); + +Comment.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Comment model'); + } +}); + exports.Comment = Comment; diff --git a/models/discussion.js b/models/discussion.js index 42ffd5bb7..f618ac8d7 100644 --- a/models/discussion.js +++ b/models/discussion.js @@ -37,10 +37,22 @@ var discussionSchema = new Schema({ _authorId: Schema.Types.ObjectId }); -discussionSchema.virtual('_since').get(function () { - return this._id.getTimestamp(); +var Discussion = mongoose.model('Discussion', discussionSchema); + +Discussion.syncIndexes(function () { + Discussion.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Discussion indexes:\n', aIndexes); + }).catch(console.error); }); -var Discussion = mongoose.model('Discussion', discussionSchema); +Discussion.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Discussion model'); + } +}); exports.Discussion = Discussion; diff --git a/models/flag.js b/models/flag.js index 601373d73..822df48f2 100644 --- a/models/flag.js +++ b/models/flag.js @@ -14,14 +14,29 @@ var Schema = mongoose.Schema; var flagSchema = new Schema({ model: String, reason: String, + weight: Number, // Natural number with the role weight of User at time of flagging _contentId: Schema.Types.ObjectId, - _userId: Schema.Types.ObjectId -}); + _userId: Schema.Types.ObjectId, -flagSchema.virtual('_since').get(function () { - return this._id.getTimestamp(); + created: Date }); var Flag = mongoose.model('Flag', flagSchema); +Flag.syncIndexes(function () { + Flag.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Flag indexes:\n', aIndexes); + }).catch(console.error); +}); + +Flag.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Flag model'); + } +}); + exports.Flag = Flag; diff --git a/models/group.js b/models/group.js index eae6db5a5..fe0391037 100644 --- a/models/group.js +++ b/models/group.js @@ -15,15 +15,28 @@ var ObjectId = Schema.Types.ObjectId; var groupSchema = new Schema({ name: { type: String }, rating: { type: Number, default: 0 }, - updated: { type: Date, default: Date.now }, + created: { type: Date }, + updated: { type: Date }, _scriptIds: [{ type: ObjectId, ref: 'Script' }], size: { type: Number, default: 0 } }); -groupSchema.virtual('_since').get(function () { - return this._id.getTimestamp(); +var Group = mongoose.model('Group', groupSchema); + +Group.syncIndexes(function () { + Group.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Group indexes:\n', aIndexes); + }).catch(console.error); }); -var Group = mongoose.model('Group', groupSchema); +Group.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Group model'); + } +}); exports.Group = Group; diff --git a/models/remove.js b/models/remove.js index 3265aeb90..19fafc491 100644 --- a/models/remove.js +++ b/models/remove.js @@ -24,4 +24,20 @@ var removeSchema = new Schema({ var Remove = mongoose.model('Remove', removeSchema); +Remove.syncIndexes(function () { + Remove.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Remove indexes:\n', aIndexes); + }).catch(console.error); +}); + +Remove.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Remove model'); + } +}); + exports.Remove = Remove; diff --git a/models/script.js b/models/script.js index 4a23fc78e..f618dd557 100644 --- a/models/script.js +++ b/models/script.js @@ -21,6 +21,7 @@ var scriptSchema = new Schema({ rating: Number, about: String, _about: String, + created: Date, updated: Date, hash: String, @@ -45,34 +46,30 @@ var scriptSchema = new Schema({ autoIndex: false }); -scriptSchema.virtual('_since').get(function () { - return this._id.getTimestamp(); -}); - /* - * Manual-indexed + * Manual indexed */ scriptSchema.index({ - isLib: 1, // A lot of hits - author: 1, // Some hits - name: 1 // Very few hits -// about: 'text' // No hits period when included... only one allowed per Schema -}); // NOTE: Array indexing isn't supported with *mongoose* (yet?) - + isLib: 1, + name: 1, + author: 1, + _description: 1, + _about: 1 +}); /* - * Auto-indexed copy + * Direct access indexed */ -// scriptSchema.index({ // NOTE: This index is currently covered in above manual compound index -// isLib: 1 -// }); - scriptSchema.index({ installName: 1 }); +/* + * Other access indexed + */ + scriptSchema.index({ _authorId: 1, flagged: 1, diff --git a/models/settings.json b/models/settings.json index 805eea49a..bb7e71e17 100644 --- a/models/settings.json +++ b/models/settings.json @@ -1,16 +1,84 @@ { "secret" : "someSecretStringForSession", - "connect" : "mongodb://dev:oujs123@ds131963.mlab.com:31963/openuserjs_devel", + "connect" : "mongodb://dev:oujs123@localhost/openuserjs_devel", + "limiter" : "mongodb://dev:oujs123@localhost:27017", "maximum_upload_script_size": 1048576, "ttl": { - "minimum": 5, + "minimum": 2, "nominal": 6, "timerSanity": 7, "timerSanityExpiry": 11, "maximum": 12 }, - "NOTE": "Requires DB migration for changing below settings", + "captchaOpts" : { + "isMath": true, + "mathMin": 999, + "mathMax": 9999, + "mathOperator": "+-", + "useFont": null, + "size": 4, + "ignoreChars": "0o1i", + "noise": 6, + "color": true, + "background": null, + "width": 200, + "height": 50, + "fontSize": 56, + "charPreset": null + }, + + "NOTE1": "WATCHPOINT: ~60 second poll time in MongoDB", + + "fudgeMin": 60, + "fudgeSec": 5, + "waitInstallCapMin": { + "dev": 1, + "pro": 60 + }, + "waitRateInstallSec": { + "dev": 30, + "pro": 60 + }, + "waitRateMetaSec": { + "dev": 30, + "pro": 60 + }, + "waitApiCapMin": { + "dev": 1, + "pro": 15 + }, + "waitAuthCapMin": { + "dev": 2, + "pro": 1440 + }, + "waitCaptchaCapMin": { + "dev": 1, + "pro": 1440 + }, + "waitListCapMin": { + "dev": 1, + "pro": 60 + }, + "waitListRateSec": { + "dev": 2, + "pro": 4 + }, + "waitListAnyQRateSec": { + "dev": 20, + "pro": 40 + }, + "waitListSameQCapMin": { + "dev": 5, + "pro": 15 + }, + "waitVoteCapMin": { + "dev": 1, + "pro": 30 + }, + + + "NOTE2": "Requires DB migration for changing below settings", "scriptSearchQueryStoreMaxDescription": 512, "scriptSearchQueryStoreMaxAbout": 256 diff --git a/models/strategy.js b/models/strategy.js index ce35bcbf3..d8bd9189e 100644 --- a/models/strategy.js +++ b/models/strategy.js @@ -20,4 +20,20 @@ var strategySchema = new Schema({ var Strategy = mongoose.model('Strategy', strategySchema); +Strategy.syncIndexes(function () { + Strategy.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Strategy indexes:\n', aIndexes); + }).catch(console.error); +}); + +Strategy.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Strategy model'); + } +}); + exports.Strategy = Strategy; diff --git a/models/sync.js b/models/sync.js new file mode 100644 index 000000000..8f7b7eb0f --- /dev/null +++ b/models/sync.js @@ -0,0 +1,47 @@ +'use strict'; + +// Define some pseudo module globals +var isPro = require('../libs/debug').isPro; +var isDev = require('../libs/debug').isDev; +var isDbg = require('../libs/debug').isDbg; + +// +var mongoose = require('mongoose'); +mongoose.Promise = global.Promise; + +var Schema = mongoose.Schema; + +var syncSchema = new Schema({ + // Visible + strat: String, // Currently github (lowercase always) + id: String, // Some unique identifier from source + target: String, // Fully Qualified URL target (should be encodeURIComponent already) + response: String, // HTTP Status Code usually and sometimes text response, + // i.e. ENOTFOUND, with no associated numeric code. Usually from dep + message: String, // Any message crafted or static + created: { type: Date, expires: 60 * 60 * 24 * 30 }, + updated: Date, + + // Extra info + _authorId: Schema.Types.ObjectId, +}); + +var Sync = mongoose.model('Sync', syncSchema); + +Sync.syncIndexes(function () { + Sync.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Sync indexes:\n', aIndexes); + }).catch(console.error); +}); + +Sync.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Sync model'); + } +}); + +exports.Sync = Sync; diff --git a/models/user.js b/models/user.js index 05a204062..9cb4c0aa3 100644 --- a/models/user.js +++ b/models/user.js @@ -6,6 +6,8 @@ var isDev = require('../libs/debug').isDev; var isDbg = require('../libs/debug').isDbg; // +var moment = require('moment'); + var mongoose = require('mongoose'); mongoose.Promise = global.Promise; @@ -15,6 +17,8 @@ var userSchema = new Schema({ // Visible name: String, about: String, + created: Date, + updated: Date, // A user can link multiple accounts to their OpenUserJS account consented: Boolean, @@ -35,11 +39,27 @@ var userSchema = new Schema({ sessionIds: [String] }); -userSchema.virtual('_since').get(function () { - return this._id.getTimestamp(); +userSchema.virtual('_probationary').get(function () { + return !moment().isAfter(moment(this.created).add(1, 'year')); }); var User = mongoose.model('User', userSchema); +User.syncIndexes(function () { + User.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('User indexes:\n', aIndexes); + }).catch(console.error); +}); + +User.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for User model'); + } +}); + exports.User = User; diff --git a/models/vote.js b/models/vote.js index e7c32baee..1ccfb2ef9 100644 --- a/models/vote.js +++ b/models/vote.js @@ -13,10 +13,28 @@ var Schema = mongoose.Schema; var voteSchema = new Schema({ vote: Boolean, + created: Date, + _scriptId: Schema.Types.ObjectId, _userId: Schema.Types.ObjectId }); var Vote = mongoose.model('Vote', voteSchema); +Vote.syncIndexes(function () { + Vote.collection.getIndexes({ + full: true + }).then(function(aIndexes) { + console.log('Vote indexes:\n', aIndexes); + }).catch(console.error); +}); + +Vote.on('index', function (aErr) { + if (aErr) { + console.error(aErr); + } else { + console.log('Index event triggered/trapped for Vote model'); + } +}); + exports.Vote = Vote; diff --git a/package.json b/package.json index 0fa0edab8..e12a5cc73 100644 --- a/package.json +++ b/package.json @@ -1,77 +1,79 @@ { "name": "OpenUserJS.org", "description": "An open source user scripts repo built using Node.js", - "version": "0.5.0", + "version": "0.6.0", "main": "app", "dependencies": { - "ace-builds": "git://github.com/ajaxorg/ace-builds.git#bd7ce25", - "ansi-colors": "4.1.1", - "async": "3.2.0", - "aws-sdk": "2.656.0", - "base62": "2.0.1", - "body-parser": "1.19.0", + "ace-builds": "1.43.6", + "animate.css": "4.1.1", + "ansi-colors": "4.1.3", + "async": "3.2.6", + "@octokit/auth-oauth-app": "4.3.1", + "aws-sdk": "2.1693.0", + "body-parser": "1.20.3", "bootstrap": "3.4.1", - "bootstrap-markdown": "2.10.0", - "clipboard": "2.0.6", - "compression": "1.7.4", - "connect-mongo": "3.2.0", - "diff": "4.0.2", - "express": "4.17.1", + "bootstrap-markdown": "git+https://github.com/OpenUserJS/bootstrap-markdown.git#marked4.x", + "clipboard": "2.0.11", + "compression": "1.8.1", + "connect-mongo": "5.1.0", + "diff": "5.2.0", + "express": "4.21.2", + "express-hcaptcha": "git+https://github.com/OpenUserJS/express-hcaptcha.git#forkUpdate", "express-minify": "1.0.0", - "express-rate-limit": "5.1.1", - "express-session": "1.17.0", + "express-rate-limit": "7.5.0", + "express-session": "1.18.1", + "express-svg-captcha": "1.0.1", "font-awesome": "4.7.0", - "formidable": "1.2.2", + "formidable": "3.5.4", "git-rev": "0.2.1", - "github": "git://github.com/octokit/rest.js.git#29dedb3", - "highlight.js": "9.18.1", - "image-size": "0.8.3", + "git-rev-sync": "3.0.2", + "github": "git+https://github.com/octokit/rest.js.git#29dedb3", + "highlight.js": "11.11.1", + "image-size": "1.2.0", "ip-range-check": "0.2.0", - "jquery": "3.5.0", - "js-beautify": "1.11.0", - "jsdom": "16.2.2", + "jquery": "3.7.1", + "js-beautify": "1.15.4", + "jsdom": "26.0.0", "less-middleware": "3.1.0", - "marked": "0.8.2", + "marked": "12.0.2", + "marked-highlight": "2.2.4", "media-type": "0.3.1", "method-override": "3.0.0", - "mime-db": "1.43.0", - "moment": "2.24.0", + "mime-db": "1.53.0", + "moment": "2.30.1", "moment-duration-format": "2.3.2", - "mongodb": "3.5.5", - "mongoose": "5.9.7", - "morgan": "1.10.0", + "mongodb": "6.14.2", + "mongoose": "5.13.23", + "morgan": "1.10.1", "mu2": "0.5.21", "octicons": "4.4.0", "passport": "0.4.1", - "passport-facebook": "3.0.0", "passport-github": "1.1.0", "passport-gitlab2": "5.0.0", "passport-google-oauth20": "2.0.0", "passport-imgur": "0.0.3", - "passport-reddit": "0.2.4", - "passport-steam": "1.0.14", - "passport-twitter": "1.0.4", - "passport-yahoo": "git://github.com/OpenUserJs/passport-yahoo.git#OpenID2", + "passport-reddit-commonjs": "1.1.0", + "passport-steam": "1.0.18", "pegjs": "0.10.0", - "rate-limit-mongo": "2.1.0", - "remark": "12.0.0", - "remark-strip-html": "1.0.1", + "rate-limit-mongo": "2.3.2", + "remark": "13.0.0", + "remark-strip-html": "1.0.2", "request": "2.88.2", - "rfc2047": "2.0.1", - "s3rver": "3.5.0", - "sanitize-html": "1.23.0", + "rfc2047": "4.0.1", + "s3rver": "3.7.1", + "sanitize-html": "2.17.2", "select2": "3.5.2-browserify", "select2-bootstrap-css": "1.4.6", - "serve-favicon": "2.5.0", - "spdx-license-ids": "3.0.5", - "strip-markdown": "3.1.2", - "terser": "4.6.11", + "serve-favicon": "2.5.1", + "spdx-license-ids": "3.0.17", + "strip-markdown": "4.2.0", + "terser": "4.8.1", "toobusy-js": "0.5.1", - "underscore": "1.10.2", + "underscore": "1.13.8", "useragent": "2.3.0" }, "optionalDependencies": { - "kerberos": "1.1.3" + "kerberos": "2.2.1" }, "repository": { "type": "git", @@ -85,8 +87,8 @@ "clean": "node dev/clean.js" }, "engines": { - "node": ">=12.13.1 <13.0.0", - "npm": ">=6.12.1" + "node": ">=24.13.0 <25.0.0", + "npm": ">=11.6.2" }, "private": true } diff --git a/public/css/common.css b/public/css/common.css index aa3b58935..74fadab37 100644 --- a/public/css/common.css +++ b/public/css/common.css @@ -130,10 +130,17 @@ h2.page-heading { position: fixed; } -.md-editor { +.md-editor, +.md-input { min-height: 300px; } +.md-editor textarea, +.md-input textarea, +.md-editor .md-preview { + margin-bottom: 10px !important; +} + textarea { width: 100%; height: 100%; @@ -199,6 +206,15 @@ textarea { margin: 0 auto; } +.form-group-signin { + margin-bottom: 0; +} + +.alert-signin { + margin-bottom: 0; + padding-top: 3.5em; +} + input#discussion-topic { display: inline-block; width: calc(100% - 18px - 4px); @@ -286,6 +302,18 @@ h6:hover a.anchor { min-width: 16px; } +.list-group-striped > a:nth-of-type(2n+1) { + background-color: #f9f9f9; +} + +.list-group-striped a.active { + background-color: #2c3e50; +} + +.list-group-striped a:hover { + background-color: #f5f5f5; +} + .table-responsive { overflow-x: auto; } @@ -350,7 +378,7 @@ ul.flaggedList { display: block; } -.shield { +.badgen { font-size: 11.475px; letter-spacing: 0.75px; color: white; @@ -360,7 +388,7 @@ ul.flaggedList { text-shadow: 0 1px #333; } -.shield a { +.badgen a { font-size: 11.25px; color: white; background-color: #0f81c2; @@ -395,7 +423,7 @@ ul.flaggedList { width: 16px; height: 16px; } -.ua-chrome, .ua-chrome-mobile, .ua-chrome-mobile-webview { +.ua-chrome, .ua-chrome-mobile, .ua-chrome-mobile-ios, .ua-chrome-mobile-webview { background: transparent url("/images/ua/chrome16.png") no-repeat left top; color: transparent; } @@ -459,7 +487,7 @@ ul.flaggedList { background: transparent url("/images/ua/rekonq16.png") no-repeat left top; color: transparent; } -.ua-safari, .ua-mobile-safari { +.ua-safari, .ua-mobile-safari, .ua-mobile-safari-ui-wkwebview { background: transparent url("/images/ua/safari16.png") no-repeat left top; color: transparent; } @@ -540,6 +568,14 @@ time.iconic, time.iconic-link { margin-top: 0; } +time.iconic-warning, time.iconic-link-warning { + box-shadow: 0 1px 0 #bdbdbd, 0 2px 0 #fff, 0 3px 0 #df691a, 0 4px 0 #fff, 0 5px 0 #df691a, 0 0 0 1px #bdbdbd; +} + +time.iconic-danger, time.iconic-link-danger { + box-shadow: 0 1px 0 #bdbdbd, 0 2px 0 #fff, 0 3px 0 #e74c3c, 0 4px 0 #fff, 0 5px 0 #e74c3c, 0 0 0 1px #bdbdbd; +} + time.iconic *, time.iconic-link * { display: block; @@ -587,6 +623,15 @@ time.iconic-link em { color: #2c3e50; } +/* Login page */ +.white-space-normal { + white-space: normal; +} + +.width-100 { + width: 100%; +} + /* Bootstrap 4.x backports */ .table > tbody > tr > td.align-middle { vertical-align: middle; diff --git a/public/images/favicon.gs.min.svg b/public/images/favicon.gs.min.svg new file mode 100644 index 000000000..b8c1ba6f6 --- /dev/null +++ b/public/images/favicon.gs.min.svg @@ -0,0 +1,2 @@ + +OpenUserJS.org faviconimage/svg+xmlOpenUserJS.org favicon2014-05-31Marti Martz (https://github.com/Martii)https://github.com/OpenUserJS/OpenUserJS.org/blob/master/LICENSEMarti Martz (https://github.com/Martii)https://raw.githubusercontent.com/OpenUserJS/OpenUserJS.org/441f6e5fc633c8c6d8c8a3d45a5868544e68dcb5/public/images/favicon.icoen-USfaviconBase SVG for faviconOpenUserJS, DoctorSnowMan, Marti Martz diff --git a/public/images/strat/reddit16.png b/public/images/strat/reddit16.png index 60dd66e42..5f38ec212 100644 Binary files a/public/images/strat/reddit16.png and b/public/images/strat/reddit16.png differ diff --git a/public/images/ua/chrome16.png b/public/images/ua/chrome16.png index acd0e21f9..82a3f48ce 100644 Binary files a/public/images/ua/chrome16.png and b/public/images/ua/chrome16.png differ diff --git a/public/images/ua/chromium16.png b/public/images/ua/chromium16.png index 98b946739..07bd4fcf0 100644 Binary files a/public/images/ua/chromium16.png and b/public/images/ua/chromium16.png differ diff --git a/public/images/ua/coccoc16.png b/public/images/ua/coccoc16.png index 3a8d517ef..ddba6a141 100644 Binary files a/public/images/ua/coccoc16.png and b/public/images/ua/coccoc16.png differ diff --git a/public/images/ua/edge16.png b/public/images/ua/edge16.png index b83c28f43..584f0ca7a 100644 Binary files a/public/images/ua/edge16.png and b/public/images/ua/edge16.png differ diff --git a/public/images/ua/firefox16.png b/public/images/ua/firefox16.png index 4ed59c201..8c16dd5f3 100644 Binary files a/public/images/ua/firefox16.png and b/public/images/ua/firefox16.png differ diff --git a/public/images/ua/opera16.png b/public/images/ua/opera16.png index 3f6e0c5b4..69cd768d4 100644 Binary files a/public/images/ua/opera16.png and b/public/images/ua/opera16.png differ diff --git a/public/images/ua/samsunginternet16.png b/public/images/ua/samsunginternet16.png index 28426e2e9..918470c52 100644 Binary files a/public/images/ua/samsunginternet16.png and b/public/images/ua/samsunginternet16.png differ diff --git a/public/images/ua/uc16.png b/public/images/ua/uc16.png index fee200d28..aaf4a9b2e 100644 Binary files a/public/images/ua/uc16.png and b/public/images/ua/uc16.png differ diff --git a/public/images/ua/vivaldi16.png b/public/images/ua/vivaldi16.png index 7302868b9..e799f1f72 100644 Binary files a/public/images/ua/vivaldi16.png and b/public/images/ua/vivaldi16.png differ diff --git a/public/less/bootstrap/oujs-variables.less b/public/less/bootstrap/oujs-variables.less index fdc89aa97..18a24c8b6 100644 --- a/public/less/bootstrap/oujs-variables.less +++ b/public/less/bootstrap/oujs-variables.less @@ -466,20 +466,20 @@ // //## Define colors for form feedback states and, by default, alerts. -@state-success-text: #3c763d; -@state-success-bg: #dff0d8; +@state-success-text: @brand-success; +@state-success-bg: #dff0d8; // NOTE: incorrect for OUJS @state-success-border: darken(spin(@state-success-bg, -10), 5%); -@state-info-text: #31708f; -@state-info-bg: #d9edf7; +@state-info-text: @brand-info; +@state-info-bg: #d9edf7; // NOTE: incorrect for OUJS @state-info-border: darken(spin(@state-info-bg, -10), 7%); -@state-warning-text: #8a6d3b; -@state-warning-bg: #fcf8e3; +@state-warning-text: @brand-warning; +@state-warning-bg: #fcf8e3; // NOTE: incorrect for OUJS @state-warning-border: darken(spin(@state-warning-bg, -10), 5%); -@state-danger-text: #a94442; -@state-danger-bg: #f2dede; +@state-danger-text: @brand-danger; +@state-danger-bg: #f2dede; // NOTE: incorrect for OUJS @state-danger-border: darken(spin(@state-danger-bg, -10), 5%); diff --git a/public/pegjs/blockOpenUserJS.pegjs b/public/pegjs/blockOpenUserJS.pegjs index c073b08bc..1b7b69923 100644 --- a/public/pegjs/blockOpenUserJS.pegjs +++ b/public/pegjs/blockOpenUserJS.pegjs @@ -1,7 +1,7 @@ // peg grammar for parsing the OpenUserJS metadata block /* -Test the generated parser with some input for peg.js site at https://pegjs.org/online: +Test the generated parser with some input for peg.js site at https://pegjs.org/online : // ==OpenUserJS== // @author Marti diff --git a/public/pegjs/blockUserLibrary.pegjs b/public/pegjs/blockUserLibrary.pegjs index e781787bf..f28739209 100644 --- a/public/pegjs/blockUserLibrary.pegjs +++ b/public/pegjs/blockUserLibrary.pegjs @@ -1,7 +1,7 @@ // peg grammar for parsing the UserLibrary metadata block /* -Test the generated parser with some input for peg.js site at https://pegjs.org/online: +Test the generated parser with some input for peg.js site at https://pegjs.org/online : // ==UserLibrary== // @name RFC 2606§3 - Hello, World! diff --git a/public/pegjs/blockUserScript.pegjs b/public/pegjs/blockUserScript.pegjs index c73403e60..d629e008f 100644 --- a/public/pegjs/blockUserScript.pegjs +++ b/public/pegjs/blockUserScript.pegjs @@ -1,7 +1,7 @@ // peg grammar for parsing the UserScript metadata block /* -Test the generated parser with some input for peg.js site at https://pegjs.org/online: +Test the generated parser with some input for peg.js site at https://pegjs.org/online : // ==UserScript== // @name RFC 2606§3 - Hello, World! @@ -22,6 +22,7 @@ Test the generated parser with some input for peg.js site at https://pegjs.org/o // @noframes // @grant GM_log // @grant none +// @connect example.com // @homepageURL http://example.com/foo/atHomepageURL1 // @homepage http://example.com/foo/atHomepage2 // @website http://example.com/foo/atSite3 @@ -45,10 +46,25 @@ Test the generated parser with some input for peg.js site at https://pegjs.org/o // @exclude http://example.com/foo // @exclude http://example.org/foo // @contributionURL https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=your.email@example.org&item_name=OpenUserJS+Author+Donation +// @antifeature ads +// @antifeature ads This script contains too many ads. +// @antifeature:ru ads Этот скрипт содержит рекламу. +// @antifeature membership +// @antifeature membership This script requires an account for full functionality. +// @antifeature miner +// @antifeature miner This script uses a lot of electricity on your behalf. +// @antifeature referral-link +// @antifeature referral-link This script makes money for the Author. +// @antifeature tracking +// @antifeature tracking This script contains a tracking of your activity. // ==/UserScript== */ +/* + * // @antifeature payment should not be supported in FOSS + */ + { var upmix = function (aKeyword) { // Keywords need to mirrored in the below rules for detection and transformation @@ -68,6 +84,8 @@ Test the generated parser with some input for peg.js site at https://pegjs.org/o case 'installURL': aKeyword = 'downloadURL'; break; + case 'domain': + aKeyword = 'connect'; } return aKeyword; @@ -91,7 +109,8 @@ line = item1Localized / items1 / - items2 + items2 / + itemz2Localized ) '\n'? { @@ -186,6 +205,7 @@ items1 = 'homepageURL' / 'homepage' / 'grant' / + 'connect' / 'exclude' / 'copyright' ) @@ -218,3 +238,43 @@ items2 = value2: value2trimmed }; } + +itemz2Localized = + keyword: + ( + 'antifeature' + ) + locale: (':' localeValue:$[a-zA-Z-]+ { + return localeValue; + })? + whitespace + value1: non_whitespace + whitespace? + value2: non_newline? + { + var keywordUpmixed = upmix(keyword); + var value2trimmed = null; + + if (value2) { + value2trimmed = value2.trim(); + } + + var obj = { + key: keywordUpmixed, + value: (value1 + (value2trimmed ? '\u0020' + value2trimmed : '')), + + value1: value1 + } + + if (value2trimmed) { + obj.value2 = value2trimmed; + } + + if (locale) { + obj.key += ':' + locale; + obj.keyword = keywordUpmixed; + obj.locale = locale; + } + + return obj; + } diff --git a/public/robots.txt b/public/robots.txt index a046a0b16..8ae0cd6ef 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,5 @@ User-agent: * +Crawl-Delay: 5 Disallow: /admin/ Disallow: /mod/ Disallow: /flag/ @@ -11,3 +12,9 @@ Disallow: /fonts/ Disallow: /js/ Disallow: /redist/ Disallow: /install/ +Disallow: /src/ +Disallow: /meta/ +Disallow: /api/ +Disallow: /post/ +Disallow: /*.js$ +Disallow: /*.user.js$ diff --git a/routes.js b/routes.js index 2f7ef5c11..5b349cd44 100644 --- a/routes.js +++ b/routes.js @@ -8,6 +8,9 @@ var isDbg = require('./libs/debug').isDbg; var rateLimit = require('express-rate-limit'); var MongoStore = require('rate-limit-mongo'); var exec = require('child_process').exec; +var hcaptcha = require('express-hcaptcha'); +var SITEKEY = process.env.HCAPTCHA_SITE_KEY; +var SECRET = process.env.HCAPTCHA_SECRET_KEY; // var main = require('./controllers/index'); @@ -25,61 +28,300 @@ var issue = require('./controllers/issue'); var scriptStorage = require('./controllers/scriptStorage'); var document = require('./controllers/document'); +var svgCaptcha = require('svg-captcha'); + +var isSameOrigin = require('./libs/helpers').isSameOrigin; +var appendUrlLeaf = require('./libs/helpers').appendUrlLeaf; + var statusCodePage = require('./libs/templateHelpers').statusCodePage; -var waitInstallMin = isDev ? 1 : 60; -var installLimiter = rateLimit({ +//--- Configuration inclusions +var settings = require('./models/settings.json'); + +//-- +var limiter = process.env.LIMITER_STRING || settings.limiter; + +var lockdown = process.env.FORCE_BUSY_UPDATEURL_CHECK === 'true'; + +// +var statusTMR = function (aReq, aRes, aNext) { + // Ignore Retry-After header here + aRes.status(429).send(); +}; + +// WATCHPOINT: ~60 second poll time in MongoDB +var fudgeMin = 60; +var fudgeSec = 6; + +var waitInstallCapMin = isDev ? settings.waitInstallCapMin.dev : settings.waitInstallCapMin.pro; +var installCapLimiter = rateLimit({ store: (isDev ? undefined : new MongoStore({ - uri: 'mongodb://127.0.0.1:27017/installLimiter', + uri: appendUrlLeaf(limiter, '/installCapLimiter'), resetExpireDateOnChange: true, // Rolling - expireTimeMs: waitInstallMin * 60 * 1000 // n minutes for mongo store + expireTimeMs: waitInstallCapMin * 60 * 1000 // n minutes for mongo store })), - windowMs: waitInstallMin * 60 * 1000, // n minutes for all stores - max: 50, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store - handler: function (aReq, aRes, aNext) { - aRes.header('Retry-After', waitInstallMin * 60 + 60); + windowMs: waitInstallCapMin * 60 * 1000, // n minutes for all stores + max: 75, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + var cmd = null; + + if (aReq.rateLimit.used < aReq.rateLimit.limit + 4) { + // Midddlware options + if (!aRes.oujsOptions) { + aRes.oujsOptions = {}; + } + + aRes.oujsOptions.showReminderInstallLimit = 4 - (aReq.rateLimit.used - aReq.rateLimit.limit); + + aNext(); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 10) { + aRes.header('Retry-After', waitInstallCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitInstallCapMin * 60 + (isDev ? fudgeSec : fudgeMin) + } + }); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 15) { + aRes.header('Retry-After', waitInstallCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); + aRes.status(429).send('Too many requests. Please try again later'); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 25) { + aRes.header('Retry-After', waitInstallCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); + aRes.status(429).send(); + } else { + cmd = (isPro && process.env.AUTOBAN ? process.env.AUTOBAN : 'echo SIMULATING INSTALL AUTOBAN') + + ' ' + aReq.connection.remoteAddress; + + exec(cmd, function (aErr, aStdout, aStderr) { + if (aErr) { + console.error('FAIL INSTALL AUTOBAN', cmd); + // fallthrough + } else { + console.log('INSTALL AUTOBAN', aReq.connection.remoteAddress); + // fallthrough + } + + aRes.connection.destroy(); + }); + } + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isMod) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + +var waitRateInstallSec = isDev ? settings.waitRateInstallSec.dev : settings.waitRateInstallSec.pro; +var installRateLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/installRateLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitRateInstallSec * 1000 // n seconds for mongo store + })), + windowMs: waitRateInstallSec * 1000, // n seconds for all stores + max: 2, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitRateInstallSec + (isDev ? fudgeSec : fudgeMin)); + if (isSameOrigin(aReq.get('Referer')).result) { + if (aReq.rateLimit.used <= aReq.rateLimit.limit + 2) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitRateInstallSec + (isDev ? fudgeSec : fudgeMin) + } + }); + return; + } + } + aRes.status(429).send(); + }, + keyGenerator: function (aReq, aRes, aNext) { + return aReq.ip + aReq._parsedUrl.pathname; + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (aReq.params.type === 'libs' && !lockdown) { + return true; + } + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + +var install1Limiter = lockdown ? installCapLimiter : installRateLimiter; +var install2Limiter = lockdown ? installRateLimiter : installCapLimiter; + +var waitRateMetaSec = isDev ? settings.waitRateMetaSec.dev : settings.waitRateMetaSec.pro; +var metaRateLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/metaRateLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitRateMetaSec * 1000 // n seconds for mongo store + })), + windowMs: waitRateMetaSec * 1000, // n seconds for all stores + max: 2, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitRateMetaSec + (isDev ? fudgeSec : fudgeMin)); + if (isSameOrigin(aReq.get('Referer')).result) { + if (aReq.rateLimit.used <= aReq.rateLimit.limit + 2) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitRateMetaSec + (isDev ? fudgeSec : fudgeMin) + } + }); + return; + } + } aRes.status(429).send(); + }, + keyGenerator: function (aReq, aRes, aNext) { + return aReq.ip + aReq._parsedUrl.pathname; + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (/\.meta\.json$/.test(aReq._parsedUrl.pathname)) { + return true; + } + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } } }); -var waitApiMin = isDev ? 1: 15; -var apiLimiter = rateLimit({ +var waitApiCapMin = isDev ? settings.waitApiCapMin.dev: settings.waitApiCapMin.pro; +var apiCapLimiter = rateLimit({ store: (isDev ? undefined : new MongoStore({ - uri: 'mongodb://127.0.0.1:27017/apiLimiter', + uri: appendUrlLeaf(limiter, '/apiCapLimiter'), resetExpireDateOnChange: true, // Rolling - expireTimeMs: waitApiMin * 60 * 1000 // n minutes for mongo store + expireTimeMs: waitApiCapMin * 60 * 1000 // n minutes for mongo store })), - windowMs: waitApiMin * 60 * 1000, // n minutes for all stores + windowMs: waitApiCapMin * 60 * 1000, // n minutes for all stores max: 100, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store - handler: function (aReq, aRes, aNext) { - aRes.header('Retry-After', waitApiMin * 60 + 60); + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitApiCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); aRes.status(429).send(); + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isMod) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + +var waitAuthCapMin = isDev ? settings.waitAuthCapMin.dev: settings.waitAuthCapMin.pro; +var authCapLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/authCapLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitAuthCapMin * 60 * 1000 // n minutes for mongo store + })), + windowMs: waitAuthCapMin * 60 * 1000, // n minutes for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitAuthCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitAuthCapMin * 60 + (isDev ? fudgeSec : fudgeMin) + } + }); + }, + skip: function (aReq, aRes) { + if (aReq.session.newstrategy) { + // NOTE: Still counting by design + return true; + } + } +}); + +var waitCaptchaCapMin = isDev ? settings.waitCaptchaCapMin.dev: settings.waitCaptchaCapMin.pro; +var captchaCapLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/captchaCapLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitCaptchaCapMin * 60 * 1000 // n minutes for mongo store + })), + windowMs: waitCaptchaCapMin * 60 * 1000, // n minutes for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.type('svg').status(200).send( + svgCaptcha('429 Too Many Requests', Object.assign(settings.captchaOpts, { + width: 350 + })) + ); + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser) { + if (authedUser.isMod) { + this.store.resetKey(this.keyGenerator); + return true; + } + + if (!authedUser._probationary) { + // NOTE: Still counting by design + return true; + } + + } } }); -var listMin = isDev ? 1: 60; -var listLimiter = rateLimit({ +var waitListCapMin = isDev ? settings.waitListCapMin.dev: settings.waitListCapMin.pro; +var listCapLimiter = rateLimit({ store: (isDev ? undefined : new MongoStore({ - uri: 'mongodb://127.0.0.1:27017/listLimiter', + uri: appendUrlLeaf(limiter, '/listCapLimiter'), resetExpireDateOnChange: true, // Rolling - expireTimeMs: listMin * 60 * 1000 // n minutes for mongo store + expireTimeMs: waitListCapMin * 60 * 1000 // n minutes for mongo store })), - windowMs: listMin * 60 * 1000, // n minutes for all stores + windowMs: waitListCapMin * 60 * 1000, // n minutes for all stores max: 115, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store - handler: function (aReq, aRes, aNext) { + handler: function (aReq, aRes, aNext, aOptions) { var cmd = null; - if (aReq.rateLimit.current < aReq.rateLimit.limit + 4) { + if (aReq.rateLimit.used < aReq.rateLimit.limit + 4) { // Midddlware options if (!aRes.oujsOptions) { aRes.oujsOptions = {}; } - aRes.oujsOptions.showReminderListLimit = 4 - (aReq.rateLimit.current - aReq.rateLimit.limit); + aRes.oujsOptions.showReminderListLimit = 4 - (aReq.rateLimit.used - aReq.rateLimit.limit); aNext(); - } else if (aReq.rateLimit.current < aReq.rateLimit.limit + 10) { - aRes.header('Retry-After', listMin * 60 + 60); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 10) { + aRes.header('Retry-After', waitListCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); statusCodePage(aReq, aRes, aNext, { statusCode: 429, statusMessage: 'Too many requests.', @@ -87,31 +329,199 @@ var listLimiter = rateLimit({ isCustomView: true, statusData: { isListView: true, - retryAfter: listMin * 60 + 60 + retryAfter: waitListCapMin * 60 + (isDev ? fudgeSec : fudgeMin) } }); - } else if (aReq.rateLimit.current < aReq.rateLimit.limit + 15) { - aRes.header('Retry-After', listMin * 60 + 60); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 15) { + aRes.header('Retry-After', waitListCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); aRes.status(429).send('Too many requests. Please try again later'); - } else if (aReq.rateLimit.current < aReq.rateLimit.limit + 20) { - aRes.header('Retry-After', listMin * 60 + 60); + } else if (aReq.rateLimit.used < aReq.rateLimit.limit + 25) { + aRes.header('Retry-After', waitListCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); aRes.status(429).send(); } else { - cmd = (isPro && process.env.AUTOBAN ? process.env.AUTOBAN : 'echo SIMULATING AUTOBAN') + + cmd = (isPro && process.env.AUTOBAN ? process.env.AUTOBAN : 'echo SIMULATING LIST AUTOBAN') + ' ' + aReq.connection.remoteAddress; exec(cmd, function (aErr, aStdout, aStderr) { if (aErr) { - console.error('FAIL AUTOBAN', cmd); + console.error('FAIL LIST AUTOBAN', cmd); // fallthrough } else { - console.log('AUTOBAN', aReq.connection.remoteAddress); + console.log('LIST AUTOBAN', aReq.connection.remoteAddress); // fallthrough } aRes.connection.destroy(); }); } + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isMod) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + +var waitListRateSec = isDev ? settings.waitListRateSec.dev : settings.waitListRateSec.pro; +var listRateLimiter = rateLimit({ + store: (isDev ? undefined : undefined), + windowMs: waitListRateSec * 1000, // n seconds for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitListRateSec + fudgeSec); + if (aReq.rateLimit.used <= aReq.rateLimit.limit + 1) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitListRateSec + fudgeSec + } + }); + return; + } + aRes.status(429).send(); + }, + keyGenerator: function (aReq, aRes, aNext) { + return aReq.ip + aReq._parsedUrl.pathname; + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (/\.meta\.json$/.test(aReq._parsedUrl.pathname)) { + return true; + } + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + + +var list1Limiter = lockdown ? listCapLimiter : listRateLimiter; +var list2Limiter = lockdown ? listRateLimiter : listCapLimiter; + + +var waitListAnyQRateSec = isDev + ? settings.waitListAnyQRateSec.dev : settings.waitListAnyQRateSec.pro; +var listAnyQRateLimiter = rateLimit({ + store: (isDev ? undefined : undefined), + windowMs: waitListAnyQRateSec * 1000, // n seconds for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitListAnyQRateSec + fudgeSec); + if (aReq.rateLimit.used <= aReq.rateLimit.limit + 2) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitListAnyQRateSec + fudgeSec + } + }); + return; + } + aRes.status(429).send(); + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } + + if (!aReq.query.q) { + return true; + } + } +}); + +var waitListSameQCapMin = isDev + ? settings.waitListSameQCapMin.dev : settings.waitListSameQCapMin.pro; +var listSameQRateLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/listSameQCapLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitListSameQCapMin * 60 * 1000 // n minutes for mongo store + })), + windowMs: waitListSameQCapMin * 60 * 1000, // n minutes for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitListSameQCapMin * 60 + (isDev ? fudgeSec : fudgeMin)); + if (aReq.rateLimit.used <= aReq.rateLimit.limit + 2) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitListSameQCapMin * 60 + (isDev ? fudgeSec : fudgeMin) + } + }); + return; + } + aRes.status(429).send(); + }, + keyGenerator: function (aReq, aRes, aNext) { + return aReq.ip + '/?' + + 'q=' + (aReq.query.q ? aReq.query.q : '') + '&' + + 'orderBy=' + (aReq.query.orderBy ? aReq.query.orderBy : '') + '&' + + 'orderDir=' + (aReq.query.orderDir ? aReq.query.orderDir : '') + '&' + + 'p=' + (aReq.query.p ? aReq.query.p : '1'); + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } + + if (!aReq.query.q) { + return true; + } + } +}); + +var waitVoteCapMin = isDev ? settings.waitVoteCapMin.dev: settings.waitVoteCapMin.pro; +var voteCapLimiter = rateLimit({ + store: (isDev ? undefined : new MongoStore({ + uri: appendUrlLeaf(limiter, '/voteCapLimiter'), + resetExpireDateOnChange: true, // Rolling + expireTimeMs: waitVoteCapMin * 60 * 1000 // n minutes for mongo store + })), + windowMs: waitVoteCapMin * 60 * 1000, // n minutes for all stores + max: 3, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + statusCodePage(aReq, aRes, aNext, { + statusCode: 429, + statusMessage: 'Too many requests.', + suppressNavigation: true, + isCustomView: true, + statusData: { + isListView: true, + retryAfter: waitAuthCapMin * 60 + (isDev ? fudgeSec : fudgeMin) + } + }); + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isMod) { + this.store.resetKey(this.keyGenerator); + return true; + } } }); @@ -121,97 +531,107 @@ module.exports = function (aApp) { //--- Routes // Authentication routes - aApp.route('/auth/').post(authentication.auth); - aApp.route('/auth/:strategy').get(authentication.auth); - aApp.route('/auth/:strategy/callback/:junk?').get(authentication.callback); - aApp.route('/login').get(main.register); - aApp.route('/register').get(function (aReq, aRes) { + aApp.route('/login').head(statusTMR).get(main.register); + aApp.route('/register').head(statusTMR).get(function (aReq, aRes) { aRes.redirect(301, '/login'); }); - aApp.route('/logout').get(main.logout); + aApp.route('/auth/').post( + authentication.preauth, + authCapLimiter, + hcaptcha.middleware.validate(SECRET, SITEKEY), + authentication.errauth, + authentication.auth + ); + aApp.route('/auth/:strategy').head(statusTMR).get(authentication.auth); + aApp.route('/auth/:strategy/callback/:junk?').head(statusTMR).get(authentication.callback); + aApp.route('/logout').head(statusTMR).get(main.logout); // User routes - aApp.route('/users').get(listLimiter, user.userListPage); - aApp.route('/users/:username').get(user.view); - aApp.route('/users/:username/comments').get(listLimiter, user.userCommentListPage); - aApp.route('/users/:username/scripts').get(listLimiter, user.userScriptListPage); - aApp.route('/users/:username/github/repos').get(authentication.validateUser, user.userGitHubRepoListPage); - aApp.route('/users/:username/github/repo').get(authentication.validateUser, user.userGitHubRepoPage); - aApp.route('/users/:username/github/import').post(authentication.validateUser, user.userGitHubImportScriptPage); - aApp.route('/users/:username/profile/edit').get(authentication.validateUser, user.userEditProfilePage).post(authentication.validateUser, user.update); - aApp.route('/users/:username/update').post(authentication.validateUser, admin.adminUserUpdate); + aApp.route('/users').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, user.userListPage); + aApp.route('/users/:username').head(statusTMR).get(user.view); + aApp.route('/users/:username/scripts').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, user.userScriptListPage); + aApp.route('/users/:username/syncs').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, user.userSyncListPage); + aApp.route('/users/:username/comments').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, user.userCommentListPage); + + aApp.route('/users/:username/github/repos').head(statusTMR).get(authentication.validateUser, user.userGitHubRepoListPage); + aApp.route('/users/:username/github/repo').head(statusTMR).get(authentication.validateUser, user.userGitHubRepoPage); + aApp.route('/users/:username/github/import').head(statusTMR).post(authentication.validateUser, user.userGitHubImportScriptPage); + + aApp.route('/users/:username/profile/edit').head(statusTMR).get(authentication.validateUser, user.userEditProfilePage).post(authentication.validateUser, user.update); + aApp.route('/users/:username/profile/captcha').head(statusTMR).get(captchaCapLimiter, authentication.validateUser, user.userEditProfilePageCaptcha); + aApp.route('/users/:username/update').head(statusTMR).post(authentication.validateUser, admin.adminUserUpdate); // NOTE: Some below inconsistent with priors - aApp.route('/user/preferences').get(authentication.validateUser, user.userEditPreferencesPage); - aApp.route('/user').get(function (aReq, aRes) { + aApp.route('/user/preferences').head(statusTMR).get(authentication.validateUser, user.userEditPreferencesPage); + aApp.route('/user').head(statusTMR).get(function (aReq, aRes) { aRes.redirect(302, '/users'); }); - aApp.route('/api/user/exist/:username').head(apiLimiter, user.exist); - aApp.route('/api/user/session/extend').post(apiLimiter, authentication.validateUser, user.extend); - aApp.route('/api/user/session/destroyOne').post(apiLimiter, authentication.validateUser, user.destroyOne); + aApp.route('/api/user/exist/:username').head(apiCapLimiter, user.exist); + aApp.route('/api/user/session/extend').post(apiCapLimiter, authentication.validateUser, user.extend); + aApp.route('/api/user/session/destroyOne').post(apiCapLimiter, authentication.validateUser, user.destroyOne); // Adding script/library routes - aApp.route('/user/add/scripts').get(listLimiter, authentication.validateUser, user.newScriptPage); - aApp.route('/user/add/scripts/new').get(authentication.validateUser, script.new(user.editScript)).post(authentication.validateUser, script.new(user.submitSource)); + aApp.route('/user/add/scripts').head(statusTMR).get(authentication.validateUser, user.newScriptPage); + aApp.route('/user/add/scripts/new').head(statusTMR).get(authentication.validateUser, script.new(user.editScript)).post(authentication.validateUser, script.new(user.submitSource)); aApp.route('/user/add/scripts/upload').post(authentication.validateUser, user.uploadScript); - aApp.route('/user/add/lib').get(authentication.validateUser, user.newLibraryPage); - aApp.route('/user/add/lib/new').get(authentication.validateUser, script.new(script.lib(user.editScript))).post(authentication.validateUser, script.new(script.lib(user.submitSource))); + aApp.route('/user/add/lib').head(statusTMR).get(authentication.validateUser, user.newLibraryPage); + aApp.route('/user/add/lib/new').head(statusTMR).get(authentication.validateUser, script.new(script.lib(user.editScript))).post(authentication.validateUser, script.new(script.lib(user.submitSource))); aApp.route('/user/add/lib/upload').post(authentication.validateUser, script.lib(user.uploadScript)); - aApp.route('/user/add').get(function (aReq, aRes) { + aApp.route('/user/add').head(statusTMR).get(function (aReq, aRes) { aRes.redirect(301, '/user/add/scripts'); }); // Script routes - aApp.route('/scripts/:username/:scriptname').get(script.view); - aApp.route('/scripts/:username/:scriptname/edit').get(authentication.validateUser, script.edit).post(authentication.validateUser, script.edit); - aApp.route('/scripts/:username/:scriptname/source').get(user.editScript); - aApp.route('/scripts/:username').get(function (aReq, aRes) { + aApp.route('/scripts/:username/:scriptname').head(statusTMR).get(script.view); + aApp.route('/scripts/:username/:scriptname/edit').head(statusTMR).get(authentication.validateUser, script.edit).post(authentication.validateUser, script.edit); + aApp.route('/scripts/:username/:scriptname/source').head(statusTMR).get(user.editScript); + aApp.route('/scripts/:username').head(statusTMR).get(function (aReq, aRes) { aRes.redirect(301, '/users/' + aReq.params.username + '/scripts'); // NOTE: Watchpoint }); - aApp.route('/install/:username/:scriptname').get(installLimiter, scriptStorage.unlockScript, scriptStorage.sendScript); + aApp.route('/install/:username/:scriptname').head(statusTMR).get(install1Limiter, install2Limiter, scriptStorage.unlockScript, scriptStorage.sendScript); - aApp.route('/meta/:username/:scriptname').get(scriptStorage.sendMeta); + aApp.route('/meta/:username/:scriptname').head(statusTMR).get(metaRateLimiter, scriptStorage.sendMeta); // Github hook routes aApp.route('/github/hook').post(scriptStorage.webhook); aApp.route('/github/service').post(function (aReq, aRes, aNext) { aNext(); }); // Library routes - aApp.route('/libs/:username/:scriptname').get(script.lib(script.view)); - aApp.route('/libs/:username/:scriptname/edit').get(authentication.validateUser, script.lib(script.edit)).post(authentication.validateUser, script.lib(script.edit)); - aApp.route('/libs/:username/:scriptname/source').get(script.lib(user.editScript)); + aApp.route('/libs/:username/:scriptname').head(statusTMR).get(script.lib(script.view)); + aApp.route('/libs/:username/:scriptname/edit').head(statusTMR).get(authentication.validateUser, script.lib(script.edit)).post(authentication.validateUser, script.lib(script.edit)); + aApp.route('/libs/:username/:scriptname/source').head(statusTMR).get(script.lib(user.editScript)); // Raw source - aApp.route('/src/:type(scripts|libs)/:username/:scriptname').get(installLimiter, scriptStorage.unlockScript, scriptStorage.sendScript); + aApp.route('/src/:type(scripts|libs)/:username/:scriptname').head(statusTMR).get(install1Limiter, install2Limiter, scriptStorage.unlockScript, scriptStorage.sendScript); // Issues routes - aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:open(open|closed|all)?').get(listLimiter, issue.list); - aApp.route('/:type(scripts|libs)/:username/:scriptname/issue/new').get(authentication.validateUser, issue.open).post(authentication.validateUser, issue.open); - aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:topic').get(listLimiter, issue.view).post(authentication.validateUser, issue.comment); - aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:topic/:action(close|reopen)').get(authentication.validateUser, issue.changeStatus); + aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:open(open|closed|all)?').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, issue.list); + aApp.route('/:type(scripts|libs)/:username/:scriptname/issue/new').head(statusTMR).get(authentication.validateUser, issue.open).post(authentication.validateUser, issue.open); + aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:topic').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, issue.view).post(authentication.validateUser, issue.comment); + aApp.route('/:type(scripts|libs)/:username/:scriptname/issues/:topic/:action(close|reopen)').head(statusTMR).get(authentication.validateUser, issue.changeStatus); // Admin routes - aApp.route('/admin').get(authentication.validateUser, admin.adminPage); - aApp.route('/admin/npm/version').get(authentication.validateUser, admin.adminNpmVersionView); - aApp.route('/admin/git/short').get(authentication.validateUser, admin.adminGitShortView); - aApp.route('/admin/git/branch').get(authentication.validateUser, admin.adminGitBranchView); - aApp.route('/admin/process/clone').get(authentication.validateUser, admin.adminProcessCloneView); - aApp.route('/admin/session/active').get(authentication.validateUser, admin.adminSessionActiveView); - aApp.route('/admin/npm/package').get(authentication.validateUser, admin.adminNpmPackageView); - aApp.route('/admin/npm/list').get(authentication.validateUser, admin.adminNpmListView); - aApp.route('/admin/api').get(authentication.validateUser, admin.adminApiKeysPage); - aApp.route('/admin/authas').get(authentication.validateUser, admin.authAsUser); - aApp.route('/admin/json').get(authentication.validateUser, admin.adminJsonView); + aApp.route('/admin').head(statusTMR).get(authentication.validateUser, admin.adminPage); + aApp.route('/admin/npm/version').head(statusTMR).get(authentication.validateUser, admin.adminNpmVersionView); + aApp.route('/admin/git/short').head(statusTMR).get(authentication.validateUser, admin.adminGitShortView); + aApp.route('/admin/git/branch').head(statusTMR).get(authentication.validateUser, admin.adminGitBranchView); + aApp.route('/admin/process/clone').head(statusTMR).get(authentication.validateUser, admin.adminProcessCloneView); + aApp.route('/admin/session/active').head(statusTMR).get(authentication.validateUser, admin.adminSessionActiveView); + aApp.route('/admin/npm/package').head(statusTMR).get(authentication.validateUser, admin.adminNpmPackageView); + aApp.route('/admin/npm/list').head(statusTMR).get(authentication.validateUser, admin.adminNpmListView); + aApp.route('/admin/api').head(statusTMR).get(authentication.validateUser, admin.adminApiKeysPage); + aApp.route('/admin/authas').head(statusTMR).get(authentication.validateUser, admin.authAsUser); + aApp.route('/admin/json').head(statusTMR).get(authentication.validateUser, admin.adminJsonView); aApp.route('/admin/api/update').post(authentication.validateUser, admin.apiAdminUpdate); // Moderation routes - aApp.route('/mod').get(authentication.validateUser, moderation.modPage); - aApp.route('/mod/removed').get(authentication.validateUser, moderation.removedItemListPage); - aApp.route('/mod/removed/:id').get(authentication.validateUser, moderation.removedItemPage); + aApp.route('/mod').head(statusTMR).get(authentication.validateUser, moderation.modPage); + aApp.route('/mod/removed').head(statusTMR).get(authentication.validateUser, moderation.removedItemListPage); + aApp.route('/mod/removed/:id').head(statusTMR).get(authentication.validateUser, moderation.removedItemPage); // Vote route - aApp.route(/^\/vote\/(scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(authentication.validateUser, vote.vote); + aApp.route(/^\/vote\/(scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(voteCapLimiter, authentication.validateUser, vote.vote); // Flag route aApp.route(/^\/flag\/(users|scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(authentication.validateUser, flag.flag); @@ -220,27 +640,27 @@ module.exports = function (aApp) { aApp.route(/^\/remove\/(users|scripts|libs)\/((.+?)(?:\/(.+))?)$/).post(authentication.validateUser, remove.rm); // Group routes - aApp.route('/groups').get(listLimiter, group.list); - aApp.route('/group/:groupname').get(listLimiter, group.view); - aApp.route('/group').get(function (aReq, aRes) { aRes.redirect('/groups'); }); - aApp.route('/api/group/search/:term/:addTerm?').get(group.search); + aApp.route('/groups').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, group.list); + aApp.route('/group/:groupname').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, group.view); + aApp.route('/group').head(statusTMR).get(function (aReq, aRes) { aRes.redirect('/groups'); }); + aApp.route('/api/group/search/:term/:addTerm?').head(statusTMR).get(group.search); // Discussion routes // TODO: Update templates for new discussion routes - aApp.route('/forum').get(listLimiter, discussion.categoryListPage); - aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss|issues|all)').get(listLimiter, discussion.list); - aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss)/:topic').get(listLimiter, discussion.show).post(authentication.validateUser, discussion.createComment); - aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss)/new').get(authentication.validateUser, discussion.newTopic).post(authentication.validateUser, discussion.createTopic); + aApp.route('/forum').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, discussion.categoryListPage); + aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss|issues|all)').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, discussion.list); + aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss)/:topic').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, discussion.show).post(authentication.validateUser, discussion.createComment); + aApp.route('/:p(forum)?/:category(announcements|corner|garage|discuss)/new').head(statusTMR).get(authentication.validateUser, discussion.newTopic).post(authentication.validateUser, discussion.createTopic); // dupe - aApp.route('/post/:category(announcements|corner|garage|discuss)').get(authentication.validateUser, discussion.newTopic).post(authentication.validateUser, discussion.createTopic); + aApp.route('/post/:category(announcements|corner|garage|discuss)').head(statusTMR).get(authentication.validateUser, discussion.newTopic).post(authentication.validateUser, discussion.createTopic); // About document routes - aApp.route('/about/:document?').get(document.view); + aApp.route('/about/:document?').head(statusTMR).get(document.view); // Home route - aApp.route('/').get(listLimiter, main.home); + aApp.route('/').head(statusTMR).get(list1Limiter, list2Limiter, listAnyQRateLimiter, listSameQRateLimiter, main.home); - // Misc API + // Misc API for cert testing aApp.route('/api').head(function (aReq, aRes, aNext) { aRes.status(200).send(); }); diff --git a/routesStatic.js b/routesStatic.js index f679ebc04..d8345314e 100644 --- a/routesStatic.js +++ b/routesStatic.js @@ -10,6 +10,32 @@ var path = require('path'); var url = require('url'); var express = require('express'); +var rateLimit = require('express-rate-limit'); + +var fudgeSec = 6; + +var waitStaticRateSec = isDev ? parseInt(4 / 2) : 4; +var staticRateLimiter = rateLimit({ + store: (isDev ? undefined : undefined), + windowMs: waitStaticRateSec * 1000, // n seconds for all stores + max: 1, // limit each IP to n requests per windowMs for memory store or expireTimeMs for mongo store + handler: function (aReq, aRes, aNext, aOptions) { + aRes.header('Retry-After', waitStaticRateSec + fudgeSec); + aRes.status(429).send(); + }, + keyGenerator: function (aReq, aRes, aNext) { + return aReq.ip + aReq._parsedUrl.pathname; + }, + skip: function (aReq, aRes) { + var authedUser = aReq.session.user; + + if (authedUser && authedUser.isAdmin) { + this.store.resetKey(this.keyGenerator); + return true; + } + } +}); + module.exports = function (aApp) { var day = 1000 * 60 * 60 * 24; @@ -35,15 +61,20 @@ module.exports = function (aApp) { path.join(dirname, aModuleBaseName, basename), { maxage: aModuleOption[basename].maxage } ) + ); } } } - aApp.use(express.static(path.join(__dirname, 'public'), { maxage: day * 1 })); + aApp.use(staticRateLimiter, express.static(path.join(__dirname, 'public'), { maxage: day * 1 })); serveModule('/redist/npm/', 'ace-builds/src/', 7); + serveModule('/redist/npm/', 'animate.css/', { + 'animate.css': { maxage: day * 1 } + }); + serveModule('/redist/npm/', 'bootstrap/', { 'dist/js/bootstrap.js': { maxage: day * 1 } }); @@ -84,7 +115,7 @@ module.exports = function (aApp) { }); serveModule('/redist/npm/', 'marked/', { - 'lib/marked.js': { maxage: day * 1 } + 'marked.min.js': { maxage: day * 1 } }); serveModule('/redist/npm/', 'octicons/', { diff --git a/views/includes/comment.html b/views/includes/comment.html index bbda5d087..781ddbc62 100644 --- a/views/includes/comment.html +++ b/views/includes/comment.html @@ -35,7 +35,7 @@ {{#canRemove}}
-
@@ -44,7 +44,7 @@ {{#flagged}}
-
@@ -52,21 +52,21 @@ {{^flagged}}
-
{{/flagged}} {{/canFlag}} {{#canEdit}} - - {{/canEdit}} - {{/authedUser}} diff --git a/views/includes/commentEditor.html b/views/includes/commentEditor.html index 1f97956e4..c8bb4866e 100644 --- a/views/includes/commentEditor.html +++ b/views/includes/commentEditor.html @@ -21,17 +21,16 @@
- + +
+
+ + + GFDL +
-
-
- - - -
-
diff --git a/views/includes/commentForm.html b/views/includes/commentForm.html index 604037213..7c098ce48 100644 --- a/views/includes/commentForm.html +++ b/views/includes/commentForm.html @@ -23,10 +23,11 @@
-
+
- + GFDL +
diff --git a/views/includes/documents/Brave.md b/views/includes/documents/Brave.md new file mode 100644 index 000000000..752efb00d --- /dev/null +++ b/views/includes/documents/Brave.md @@ -0,0 +1,24 @@ +## Brave + + +Brave is a free web browser based off of [Chromium][chromium]. Brave's selling points are more security, better privacy, speed, simplicity and stability. The desktop version runs on Windows, Linux and OS X, and there are mobile versions for Android and iOS. + +To run userscripts on Brave, you normally need a manager extension. + +* [Get Brave][braveBrowser] +* [Brave on Wikipedia][wikipediaBrave] +* [Brave support][braveSupport] +* [Brave development][braveDevelopment] +* [Tampermonkey for Brave][tampermonkeyForBrave] +* [Violentmonkey for Brave][violentmonkeyForBrave] + +[githubFavicon]: https://assets-cdn.github.com/favicon.ico +[oujsFavicon]: https://raw.githubusercontent.com/OpenUserJs/OpenUserJS.org/master/public/images/favicon16.png +[chromium]: Chromium +[braveBrowser]: https://brave.com/ +[wikipediaBrave]: https://www.wikipedia.org/wiki/Brave_(web_browser) +[braveSupport]: https://support.brave.app/hc +[braveDevelopment]: https://github.com/brave/brave-browser +[tampermonkeyForBrave]: Tampermonkey-for-Brave +[violentmonkeyForBrave]: Violentmonkey-for-Brave +[chromium]: Chromium diff --git a/views/includes/documents/Falkon.md b/views/includes/documents/Falkon.md index 0f72dbb70..bcceb7490 100644 --- a/views/includes/documents/Falkon.md +++ b/views/includes/documents/Falkon.md @@ -45,9 +45,9 @@ Sometimes, when you use more than one userscript on the same web page, they need #### Debugging -Curently to enable the remote debugging feature in Falkon with Ctrl + Shift + i an environment variable with a name of `QTWEBENGINE_REMOTE_DEBUGGING` needs to be set with an available numeric TCP port such as `12345`. This applies to all platforms. +Enabling the remote debugging feature with Ctrl + Shift + i an environment variable may need to be created in some versions of Falkon using a name of `QTWEBENGINE_REMOTE_DEBUGGING` and an available numeric TCP port such as `12345`. This applies to all available platforms. -#### Compiling +#### Optional Compiling After installing the necessary dependencies: @@ -56,7 +56,7 @@ After installing the necessary dependencies: ``` sh-session $ # Anonymous checkout -$ git clone git://anongit.kde.org/falkon.git +$ git clone git@invent.kde.org:network/falkon.git $ cd falkon $ # Cleanup build directory if needed for cmake pitfalls $ rm -Rf build @@ -80,6 +80,7 @@ Patches are usually submitted by using `$ git diff > mypatch.diff` to KDE. ### More * [Get Falkon][falkonBrowser] + * **NOTE:** At the time of writing the flatpak repository appears to have a newer version than the snap repository. The snap repository may not include the GreaseMonkey extension. See their download link for specific package repositories. * [Falkon HTML Repo][falkonHTMLRepo] * [KDE Bugs Issue Tracker][kdeIssueTracker] [*(Falkon Issues)*][kdeIssueTrackerFalkonIssues] * [Falkon Mailing List][falkonMailingList] [*(Subscribe here)*][falkonMailingListSubscribe] @@ -98,7 +99,7 @@ Patches are usually submitted by using `$ git diff > mypatch.diff` to KDE. [falkonScreenshot5]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/falkon5.png "Script management" [falkonBrowser]: https://www.falkon.org/ -[falkonHTMLRepo]: https://cgit.kde.org/falkon.git/ +[falkonHTMLRepo]: https://invent.kde.org/network/falkon [kdeIssueTracker]: https://bugs.kde.org/ [kdeIssueTrackerFalkonIssues]: https://bugs.kde.org/buglist.cgi?product=Falkon [falkonMailingList]: mailto:falkon@kde.org diff --git a/views/includes/documents/Frequently-Asked-Questions.md b/views/includes/documents/Frequently-Asked-Questions.md index d960c4b24..8eaa15058 100644 --- a/views/includes/documents/Frequently-Asked-Questions.md +++ b/views/includes/documents/Frequently-Asked-Questions.md @@ -171,7 +171,7 @@ In order to minimize the occurence of these, and speed up the page load for you ``` diff @@ -8,10 +8,18 @@ - // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt + // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // ==/UserScript== -this.$ = this.jQuery = jQuery.noConflict(true); @@ -219,12 +219,12 @@ Multiple forms exist for various purposes: 1. `.meta.js` - This is the traditional `// @` delimited usage that outputs **some** of the metadata blocks items from a userscript for updating in userscript engines such as [Greasemonkey][greasemonkeyForFirefox] and is used with `@updateURL`. * [https://openuserjs.org/**meta**/username/scriptname.meta.js][metaJSExample] - * This is the preferred route and goes directly to the necessary items needed for updating. This route is currently unmanaged. If you want your update checks faster most of the time this is the route to choose. + * This is the preferred route and goes directly to the necessary items needed for updating. This route is currently minimally managed. If you want your update checks faster most of the time this is the route to choose. * [https://openuserjs.org/**install**/username/scriptname.meta.js][metaJSExample2] - * This is the legacy route and indirectly goes to the necessary items needed for updating. This route is currently managed. If you want your script update checks to potentially not come during high traffic times this is the route to choose. - * One of these is currently required when OpenUserJS is in lockdown mode. If any script points to an OUJS .user.js url it will not be served. If it is absent it will not be served. Occasionally a script and/or .user.js engine might put out a bugged version and in order to ensure minimal site disruption OUJS may optionally toggle into lockdown mode. Hopefully these instances will be few but there is existing precedence for this use case. Please see [About][oujsAbout] for current site status and your Author Tools panel on each of your scripts source page. + * This is the legacy route and indirectly goes to the necessary items needed for updating. This route is currently heavily managed. If you want your script update checks to potentially not come during high traffic times this is the route to choose. + * One of these is currently required when OpenUserJS is in lockdown mode. If any script points to an OUJS .user.js url it will not be served. If it is absent it will not be served. Occasionally a bad actor, script, and/or .user.js engine might put out a problematic version. In order to ensure minimal site disruption OUJS may optionally toggle into lockdown mode at any time. Hopefully these instances will be few but there is ample existing precedence for these use cases. Please see [About][oujsAbout] for current site status and your Author Tools panel on each of your scripts source page. Lockdown starts when it starts and it ends when it ends. * You must choose. But choose wisely, for as the true .meta.js will bring you life, a false one will take it from you. -2. `.meta.json` - This is the modern [JSON][JSONHomepage] usage that outputs the information we collect from the metadata blocks. This is **not** currently intended to be used in any metadata block but rather, on a specific occasion, used programmatically in a User Script. At this time there is no known .user.js engine that supports this feature directly. +2. `.meta.json` - This is the modern [JSON][JSONHomepage] usage that outputs the information we collect from the metadata blocks. This is **not** currently intended to be used in any metadata block but rather, on a specific occasion, used programmatically in a User Script or using third party badges. At this time there is no known .user.js engine that supports this feature directly. * [https://openuserjs.org/**meta**/username/scriptname.meta.json][metaJSONExample] 3. `.user.js` + `text/x-userscript-meta` - Modern Userscript engines **sometimes** may send a special header out in order to retrieve just the meta. * [https://openuserjs.org/**install**/username/scriptname.user.js][userJSExampleBrokenAsIntended] plus the Userscript engine sending out the request header. @@ -247,6 +247,81 @@ A: Yes, use the raw source route like this in the UserScript metadata block: The `@downloadURL` UserScript metadata block key is not currently required but highly encouraged especially due to potential faulty .user.js engine updaters. +### Q: What is antifeature? + +A: Adopted and vetted from another site, this UserScript metadata block key indicates what types of Author indicated beneficial Code has been included with the script. This allows any visitor to make a more informed decision before installation. + +Non-localized usage: +``` js +// @antifeature type comment +``` + +Localized usage: +``` js +// @antifeature:cs type komentář. +// @antifeature:es-MX type comentario. +// @antifeature:ru type комментарий + +``` + +* There can be any number of @antifeature keys in a script. +* `comment` is optional but recommended. Comments will show up as a tooltip for all unlocalized and localized values. + + +The following key type(s) are currently supported: + +#### ads antifeature + +``` js +// @antifeature ads This script contains too many ads. +``` + +#### membership antifeature + +``` js +// @antifeature membership This script requires an account for full functionality. +``` + +#### miner antifeature + +``` js +// @antifeature miner This script uses a lot of electricity on your behalf. +``` + +#### referral-link antifeature + +``` js +// @antifeature referral-link This script makes money for the Author. +``` + + +#### tracking antifeature + +``` js +// @antifeature tracking This script contains a tracking of your activity. +``` + +The following key type(s) are currently __not__ supported: + +#### payment antifeature + +``` js +// @antifeature payment This script utilizes additional monetary proprietary upstream software and access. +``` + +If any of these keys are present then additional consideration should be utilized for moderation requests. If any key is absent and a script is found to contain Code relevant to these types please flag the script for moderation inspection with specifics. + + +### Q: Does OpenUserJS.org have script synchronization from a version control site? + +This site is currently a presentational userscript repository and is intended to distribute the final published product in a familiar community setting. + +However if you need, or want, the highly recommended [version control][wikipediaSCM] for prior versions of your script please add an additional, supported, authentication strategy in [your account preferences][oujsPreferences] or when you first sign up. This enables you to have the available import and synchronization options described on the respective pages accessed from your accounts username. The site will only attempt to synchronize to your existing script already on the site. Please ensure there is, at minimum, the same metadata block(s) present you want to synchronize from. + +Current supported synchronizable authentication strategies are: + +* GitHub + [greasemonkeyForFirefox]: Greasemonkey-for-Firefox [metaJSExample]: https://openuserjs.org/meta/Marti/oujs_-_Meta_View.meta.js [metaJSExample2]: https://openuserjs.org/install/Marti/oujs_-_Meta_View.meta.js @@ -254,7 +329,8 @@ The `@downloadURL` UserScript metadata block key is not currently required but h [userJSExampleBrokenAsIntended]: https://openuserjs.org/install/Marti/.user.js [oujsMetaViewExample]: https://openuserjs.org/scripts/Marti/oujs_-_Meta_View [oujsAbout]: https://openuserjs.org/about -[JSONHomepage]: http://json.org/ +[oujsPreferences]: https://openuserjs.org/user/preferences +[JSONHomepage]: https://json.org/ [wikipediaSCM]: https://www.wikipedia.org/wiki/Version_control [gitSCM]: https://git-scm.com/ [gitSCMdoc]: https://git-scm.com/doc diff --git a/views/includes/documents/Greasemonkey-Port-for-SeaMonkey.md b/views/includes/documents/Greasemonkey-Port-for-SeaMonkey.md index c11fb522d..4009b08b2 100644 --- a/views/includes/documents/Greasemonkey-Port-for-SeaMonkey.md +++ b/views/includes/documents/Greasemonkey-Port-for-SeaMonkey.md @@ -48,7 +48,6 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Get Greasemonkey Port from SourceForge][sfGreasemonkeyPort] * [SourceForge Greasemonkey/Port Wiki][greasemonkeyPortWiki] * [OpenUserJS.org Greasemonkey Port Update Announcements][oujsGMPUpdateAnnouncement] -* [Additional older versions for SeaMonkey][xsidebarModGM] * [Greasespot.net][greasespot] - blog, documentation and discussion about Greasemonkey. [githubFavicon]: https://assets-cdn.github.com/favicon.ico @@ -56,7 +55,6 @@ Sometimes, when you use more than one userscript on the same web page, they need [oujs]: https://openuserjs.org/ [oujsGMPUpdateAnnouncement]: /announcements/Greasemonkey_Port_Update [sfGreasemonkeyPort]: https://sourceforge.net/projects/gmport/ -[xsidebarModGM]: http://xsidebar.mozdev.org/modifiedmisc.html#greasemonkey [aboutAddons]: about:addons [aomUserScriptsScreenshot]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/greasemonkeyport5.png "Userscript management in SeaMonkey" [greasespot]: http://www.greasespot.net/ diff --git a/views/includes/documents/Pale-Moon.md b/views/includes/documents/Pale-Moon.md index ce0396fdf..9ea203e1f 100644 --- a/views/includes/documents/Pale-Moon.md +++ b/views/includes/documents/Pale-Moon.md @@ -9,13 +9,13 @@ While there is no official ported Greasmonkey for Pale Moon there is some activi * [Get Pale Moon][palemoonBrowser] * [Pale Moon on Wikipedia][wikipediaPalemoon] -* [UXP (with Pale Moon) Issue Tracker][palemoonIssueTracker] +* [Issue Tracker for Pale Moon][palemoonIssueTracker] [githubFavicon]: https://assets-cdn.github.com/favicon.ico [oujsFavicon]: https://raw.githubusercontent.com/OpenUserJs/OpenUserJS.org/master/public/images/favicon16.png [palemoonBrowser]: http://www.palemoon.org/ [wikipediaPalemoon]: https://www.wikipedia.org/wiki/Pale_Moon_%28web_browser%29 -[palemoonIssueTracker]: https://github.com/MoonchildProductions/UXP/issues +[palemoonIssueTracker]: https://forum.palemoon.org/viewforum.php?f=5 [greasemonkeyForkRepo]: https://github.com/janekptacijarabaci/greasemonkey [greasemonkeyForkReleases]: https://github.com/janekptacijarabaci/greasemonkey/releases [greasemonkeyForkLTS]: https://github.com/janekptacijarabaci/greasemonkey/issues/1 diff --git a/views/includes/documents/Tampermonkey-for-Android.md b/views/includes/documents/Tampermonkey-for-Android.md index 337ae04d1..1a6a8ae35 100644 --- a/views/includes/documents/Tampermonkey-for-Android.md +++ b/views/includes/documents/Tampermonkey-for-Android.md @@ -59,6 +59,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Get Tampermonkey from the Google Play Store][gooPlayStoreTampermonkey] * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Edge][tampermonkeyForEdge] @@ -94,6 +95,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyAndroidScreenshot4]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/tampermonkey_an5.png "Tampermonkey Options" +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge diff --git a/views/includes/documents/Tampermonkey-for-Brave.md b/views/includes/documents/Tampermonkey-for-Brave.md new file mode 100644 index 000000000..17cb5ec82 --- /dev/null +++ b/views/includes/documents/Tampermonkey-for-Brave.md @@ -0,0 +1,91 @@ +## Tampermonkey for Brave + + +Tampermonkey is a userscript manager extension for [Android][android], [Chrome][Chrome], [Chromium][Chromium], [Edge][Edge], [Firefox][firefox], [Opera][Opera], [Safari][Safari], and other similar web browsers, written by Jan Biniok. There are also versions for [Android][tampermonkeyForAndroid], [Chrome][tampermonkeyForChrome], [Chromium][tampermonkeyForChromium], [Edge][tampermonkeyForEdge], [Firefox][tamperMonkeyForFirefox], [Opera][tampermonkeyForOpera], and [Safari][tampermonkeyForSafari]. + +### Installing Tampermonkey + +To get userscripts going with the desktop version of Tampermonkey, first you have to install it from the [Chrome Web Store][gooChromeWebStoreTampermonkey]. + +![Screenshot of Tampermonkey page in Chrome Web Store][tampermonkeyGooChromeWebStoreScreenshot] + +From the Tampermonkey page in the Chrome Store, click the blue "Add to Brave" button to install the extension. Brave will ask you to confirm the extension. Click "Add extension". + +Once Tampermonkey has finished installing, you should see a confirmation page from the Tampermonkey website and briefly a pop-up confirming that Tampermonkey has been added to Brave. This should point to a new extension puzzle piece, near the top of the Brave window, next to the address bar. Inside the menu will be Tampermonkey. + +### Installing Userscripts + +Once Tampermonkey is installed, installing userscripts from [OpenUserJS.org][oujs] is simple. Navigate to the OpenUserJS page for the script, then click the blue "Install" button at the top of the page. + +![Screenshot of an OpenUserJS script page][oujsScriptPageScreenshot1] + +Tampermonkey will display a screen showing you where the userscript has come from, what websites it can access, its source code, and a warning to only install scripts from sources that you trust. If you do want to install the script, click the "Install" button, otherwise click "Cancel". + +![Screenshot of Tampermonkey script installation][tampermonkeyBraveScreenshot3] + +Installing userscripts from other sources is a similar process. You just need to find the installation link for the script. This will be a button or link to a file with a name that ends ".user.js" + +After installing a userscript, you won't normally notice any further changes until you visit a website that it runs on. + +### Managing Userscripts + +Clicking on the Tampermonkey icon at any time will pop up a menu that shows you what userscripts are running on the website you are looking at. It also lets you check for updated scripts *(it does daily automatic checks by default)*, and open the Tampermonkey Dashboard. + +![Screenshot of Tampermonkey Dashboard][tampermonkeyBraveScreenshot4] + +In the Dashboard, the "Installed scripts" tab is the main place to manage your userscripts. The numbered circle to the left of each script shows you the order they run in, and whether they are enabled *(green)* or disabled *(red)* - click it to toggle the status. You can also uninstall userscripts *(trash can icon)*, or check for new updates *(click the "last updated" date)*. + +### Trouble shooting + +If you think a userscript is causing problems, the easiest way to check is to switch off Tampermonkey, reload the web page, and see if the symptoms go away. You can do this by clicking on the Tampermonkey icon then clicking "Enabled"; the tick icon should change to a cross. If it looks like a script problem and you have more than one script running on a web page, you can disable them all in Tampermonkey's dashboard then re-enable them one by one, until you find the culprit. Remember to reload the web page each time - userscripts normally only run when a web page loads. + +Sometimes, when you use more than one userscript on the same web page, they need to run in a particular order. You can change the order using the Tampermonkey dashboard. In the "Sort order" column, click on the 'three lines' icon for the script you want to move, move the mouse up or down to change the order, then click again. + +### More + +* [Get Tampermonkey from the Chrome Web Store][gooChromeWebStoreTampermonkey] +* [Get Tampermonkey Beta from the Chrome Web Store][gooChromeWebStoreTampermonkeyBeta] +* [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. + +* [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Chromium][tampermonkeyForChromium] +* [Tampermonkey for Chrome][tampermonkeyForChrome] +* [Tampermonkey for Edge][tampermonkeyForEdge] +* [Tampermonkey for Opera][tampermonkeyForOpera] +* [Tampermonkey for Safari][tampermonkeyForSafari] + + + + +[githubFavicon]: https://assets-cdn.github.com/favicon.ico +[oujsFavicon]: https://raw.githubusercontent.com/OpenUserJs/OpenUserJS.org/master/public/images/favicon16.png +[oujs]: https://openuserjs.org/ + + +[android]: Android +[chrome]: Chrome +[chromium]: Chromium +[edge]: Edge +[firefox]: Firefox +[opera]: Opera +[safari]: Safari + + +[tampermonkeyNet]: http://tampermonkey.net/ +[gooChromeWebStoreTampermonkey]: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo +[gooChromeWebStoreTampermonkeyBeta]: https://chrome.google.com/webstore/detail/tampermonkey-beta/gcalenpjmijncebpfijmoaglllgpjagf + + +[tampermonkeyGooChromeWebStoreScreenshot]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/tampermonkey_br.gif "Tampermonkey in the Chrome Web Store" +[oujsScriptPageScreenshot1]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/openuserjs_script.gif "Ready to install a script" +[tampermonkeyBraveScreenshot3]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/tampermonkey_br4.gif "Installing a script" +[tampermonkeyBraveScreenshot4]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/tampermonkey_br5.png "Tampermonkey Dashboard" + + +[tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForChrome]: Tampermonkey-for-Chrome +[tampermonkeyForChromium]: Tampermonkey-for-Chromium +[tampermonkeyForEdge]: Tampermonkey-for-Edge +[tampermonkeyForFirefox]: Tampermonkey-for-Firefox +[tampermonkeyForOpera]: Tampermonkey-for-Opera +[tampermonkeyForSafari]: Tampermonkey-for-Safari diff --git a/views/includes/documents/Tampermonkey-for-Chrome.md b/views/includes/documents/Tampermonkey-for-Chrome.md index d902437be..65eb21b6c 100644 --- a/views/includes/documents/Tampermonkey-for-Chrome.md +++ b/views/includes/documents/Tampermonkey-for-Chrome.md @@ -52,6 +52,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Edge][tampermonkeyForEdge] * [Tampermonkey for Opera][tampermonkeyForOpera] @@ -88,6 +89,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge [tampermonkeyForFirefox]: Tampermonkey-for-Firefox diff --git a/views/includes/documents/Tampermonkey-for-Chromium.md b/views/includes/documents/Tampermonkey-for-Chromium.md index 7b14f079a..4c2fd40bb 100644 --- a/views/includes/documents/Tampermonkey-for-Chromium.md +++ b/views/includes/documents/Tampermonkey-for-Chromium.md @@ -48,6 +48,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Edge][tampermonkeyForEdge] * [Tampermonkey for Opera][tampermonkeyForOpera] @@ -82,6 +83,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForEdge]: Tampermonkey-for-Edge [tampermonkeyForFirefox]: Tampermonkey-for-Firefox diff --git a/views/includes/documents/Tampermonkey-for-Edge.md b/views/includes/documents/Tampermonkey-for-Edge.md index 0d59eb62a..815042360 100644 --- a/views/includes/documents/Tampermonkey-for-Edge.md +++ b/views/includes/documents/Tampermonkey-for-Edge.md @@ -49,6 +49,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Opera][tampermonkeyForOpera] @@ -72,7 +73,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyNet]: http://tampermonkey.net/ -[edgeAddons]: https://www.microsoft.com/store/apps/9NBLGGH5162S +[edgeAddons]: https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd [tampermonkeyMSWebStoreScreenshot]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/tampermonkey_edge.gif "Tampermonkey in the Windows Store" @@ -82,6 +83,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForFirefox]: Tampermonkey-for-Firefox diff --git a/views/includes/documents/Tampermonkey-for-Firefox.md b/views/includes/documents/Tampermonkey-for-Firefox.md index 9b810ff72..3f3501f00 100644 --- a/views/includes/documents/Tampermonkey-for-Firefox.md +++ b/views/includes/documents/Tampermonkey-for-Firefox.md @@ -46,6 +46,7 @@ Sometimes, usually on a portable device, it may look like the userscript manager * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Edge][tampermonkeyForEdge] @@ -84,6 +85,7 @@ Sometimes, usually on a portable device, it may look like the userscript manager [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge diff --git a/views/includes/documents/Tampermonkey-for-Opera.md b/views/includes/documents/Tampermonkey-for-Opera.md index a230df96e..a93aea0c4 100644 --- a/views/includes/documents/Tampermonkey-for-Opera.md +++ b/views/includes/documents/Tampermonkey-for-Opera.md @@ -46,6 +46,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Edge][tampermonkeyForEdge] @@ -81,6 +82,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge diff --git a/views/includes/documents/Tampermonkey-for-Safari.md b/views/includes/documents/Tampermonkey-for-Safari.md index ee500c8f2..3d3cc3741 100644 --- a/views/includes/documents/Tampermonkey-for-Safari.md +++ b/views/includes/documents/Tampermonkey-for-Safari.md @@ -49,6 +49,7 @@ Sometimes, when you use more than one userscript on the same web page, they need * [Tampermonkey.net][tampermonkeyNet] - documentation, discussion and downloads for other versions of Tampermonkey. * [Tampermonkey for Android][tampermonkeyForAndroid] +* [Tampermonkey for Brave][tampermonkeyForBrave] * [Tampermonkey for Chrome][tampermonkeyForChrome] * [Tampermonkey for Chromium][tampermonkeyForChromium] * [Tampermonkey for Edge][tampermonkeyForEdge] @@ -84,6 +85,7 @@ Sometimes, when you use more than one userscript on the same web page, they need [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge diff --git a/views/includes/documents/Userscript-Beginners-HOWTO.md b/views/includes/documents/Userscript-Beginners-HOWTO.md index ab6da0514..e81243d9e 100644 --- a/views/includes/documents/Userscript-Beginners-HOWTO.md +++ b/views/includes/documents/Userscript-Beginners-HOWTO.md @@ -5,14 +5,14 @@ These pages are a work in progress, but are intended to help users with no previ ### What is a User Script? -Userscripts *(a.k.a User Scripts, User scripts, or `.user.js`)* are open-source licensed add-ons for web browsers that change web pages as they are loaded. They give users the power to make websites do what they want them to, rather than what was originally intended. This kind of script is usually file named on your computer as `site it affects - what useful name you want to call it.user.js` and always **does** end in `.user.js`. +Userscripts *(a.k.a User Scripts, User scripts, or `.user.js`)* are open-source licensed add-ons for web browsers, written in [JavaScript][wikipediaJavaScript], that change web pages as they are loaded. They give users the power to make websites do what they want them to, rather than what was originally intended. This kind of script is usually file named on your computer as `site it affects - what useful name you want to call it.user.js` and always **does** end in `.user.js`. Useful tasks like improving layout, fixing bugs, automating common tasks and adding new functions can all be done by userscripts. More complicated userscripts can create mash-ups by combining information from different websites or embedding new data into a web page, e.g. to add reviews or price comparisons to a shopping website. ### What is a Library Script? -Library scripts *(a.k.a libs, libraries, or plain `.js`)* are reusable open-source licensed pieces of code that are shared for common uses in other Userscripts. This kind of script is usually file named `what useful name you want to call it.js` and always **does not** end in `.user.js`. +Library scripts *(a.k.a libs, libraries, or plain `.js`)* are reusable open-source licensed pieces of [JavaScript][wikipediaJavaScript] code that are shared for common uses in other Userscripts. This kind of script is usually file named `what useful name you want to call it.js` and always **does not** end in `.user.js`. ### How do I use a userscript? @@ -31,9 +31,11 @@ Find your web browser in the table below, and follow the links to find out the o        | [Android][android] | [ iOS ][ios] | [Linux][linux] | [ macOS ][macos] | [Windows][windows] --- | :---: | :---: | :---: | :---: | :---:   | -**[Chrome][chrome]** | [Tampermonkey][tampermonkeyForAndroid] | – | [Tampermonkey][tampermonkeyForChrome], [Violentmonkey][violentmonkeyForChrome] | [Tampermonkey][tampermonkeyForChrome], [Violentmonkey][violentmonkeyForChrome] | [Tampermonkey][tampermonkeyForChrome], [Violentmonkey][violentmonkeyForChrome] +**[Brave][brave]** | – | – | [Tampermonkey][tampermonkeyForBrave], [Violentmonkey][violentmonkeyForBrave] | [Tampermonkey][tampermonkeyForBrave], [Violentmonkey][violentmonkeyForBrave] | [Tampermonkey][tampermonkeyForBrave], [Violentmonkey][violentmonkeyForBrave]   | -**[Chromium][chromium]** | – | – | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] +**[Chrome][chrome]** | [Tampermonkey][tampermonkeyForAndroid] | – | [Tampermonkey][tampermonkeyForChrome] | [Tampermonkey][tampermonkeyForChrome] | [Tampermonkey][tampermonkeyForChrome] +  | +**[Chromium][chromium]** | – | – | [Tampermonkey][tampermonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium]   | **[Edge][edge]** | – | – | – | – | [Tampermonkey][tampermonkeyForEdge]   | @@ -49,7 +51,7 @@ Find your web browser in the table below, and follow the links to find out the o   | **[SeaMonkey][seamonkey]** | – | – | [Greasemonkey Port][greasemonkeyPortForSeaMonkey]| [Greasemonkey Port][greasemonkeyPortForSeaMonkey]| [Greasemonkey Port][greasemonkeyPortForSeaMonkey]   | -**[Yandex Browser][yandexbrowser]** | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] | – | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium], [Violentmonkey][violentmonkeyForChromium] +**[Yandex Browser][yandexbrowser]** | [Tampermonkey][tampermonkeyForChromium] | – | [Tampermonkey][tampermonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium] | [Tampermonkey][tampermonkeyForChromium] [githubFavicon]: https://assets-cdn.github.com/favicon.ico [oujsFavicon]: https://raw.githubusercontent.com/OpenUserJs/OpenUserJS.org/master/public/images/favicon16.png @@ -58,6 +60,7 @@ Find your web browser in the table below, and follow the links to find out the o [greasemonkeyPortForSeaMonkey]: Greasemonkey-Port-for-SeaMonkey [tampermonkeyForAndroid]: Tampermonkey-for-Android +[tampermonkeyForBrave]: Tampermonkey-for-Brave [tampermonkeyForChrome]: Tampermonkey-for-Chrome [tampermonkeyForChromium]: Tampermonkey-for-Chromium [tampermonkeyForEdge]: Tampermonkey-for-Edge @@ -65,8 +68,7 @@ Find your web browser in the table below, and follow the links to find out the o [tampermonkeyForOpera]: Tampermonkey-for-Opera [tampermonkeyForSafari]: Tampermonkey-for-Safari -[violentmonkeyForChrome]: Violentmonkey-for-Chrome -[violentmonkeyForChromium]: Violentmonkey-for-Chromium +[violentmonkeyForBrave]: Violentmonkey-for-Brave [violentmonkeyForFirefox]: Violentmonkey-for-Firefox [violentmonkeyForOpera]: Violentmonkey-for-Opera @@ -76,6 +78,7 @@ Find your web browser in the table below, and follow the links to find out the o [macos]: macOS [windows]: Windows +[brave]: Brave [chrome]: Chrome [chromium]: Chromium [edge]: Edge @@ -86,3 +89,5 @@ Find your web browser in the table below, and follow the links to find out the o [safari]: Safari [seamonkey]: SeaMonkey [yandexbrowser]: Yandex-Browser + +[wikipediaJavaScript]: https://www.wikipedia.org/wiki/JavaScript diff --git a/views/includes/documents/Violentmonkey-for-Chromium.md b/views/includes/documents/Violentmonkey-for-Brave.md similarity index 72% rename from views/includes/documents/Violentmonkey-for-Chromium.md rename to views/includes/documents/Violentmonkey-for-Brave.md index 872a44e27..a0c1354f0 100644 --- a/views/includes/documents/Violentmonkey-for-Chromium.md +++ b/views/includes/documents/Violentmonkey-for-Brave.md @@ -1,15 +1,15 @@ -## Violentmonkey for Chromium - +## Violentmonkey for Brave + -Violentmonkey is a userscript manager for the [Chrome][chrome], [Chromium][chromium], [Firefox][firefox], [Opera][opera] web browser, written by [gera2ld][gera2ld]. +Violentmonkey is a userscript manager for the [Brave][brave], [Firefox][firefox], and [Opera][opera] web browser and formerly the [Chrome][chrome], [Chromium][chromium] browsers using Manifest v2.x, written by [gera2ld][gera2ld]. ### Installing Violentmonkey To get userscripts going with the desktop version of Violentmonkey, first you have to install it from the [Chrome Add-ons website][chromeAddons]. -![Screenshot of Violentmonkey page in Chromium Add-ons website][chromeAddonsScreenshot1] +![Screenshot of Violentmonkey page in Brave Add-ons website][braveAddonsScreenshot1] -From the Violentmonkey page in the Chromium Add-ons website, click the green "Add to Chromium" button to install the extension. Once Violentmonkey has finished installing, you should see a pop-up confirming that Violentmonkey has been added to Chromium. This should point to a new Violentmonkey icon at the top of the Chromium window, next to the address bar. +From the Violentmonkey page in the Chromium Add-ons website, click the green "Add to Brave" button to install the extension. Once Violentmonkey has finished installing, you should see a pop-up confirming that Violentmonkey has been added to Brave. This should point to a new extension puzzle piece, near the top of the Brave window, next to the address bar. Inside the menu will be Violentmonkey. ### Installing Userscripts @@ -19,7 +19,7 @@ Once Violentmonkey is installed, installing userscripts from [OpenUserJS.org][ou Violentmonkey will display a screen showing you the source code of the userscript. Click the "Confirm installation" button to finish installing the script. -![Screenshot of Violentmonkey script installation][violentmonkeyChromiumScreenshot2] +![Screenshot of Violentmonkey script installation][violentmonkeyBraveScreenshot2] Installing userscripts from other sources is a similar process. You just need to find the installation link for the script. This will be a button or link to a file with a name that ends ".user.js" @@ -29,8 +29,7 @@ After installing a userscript, you won't normally notice any further changes unt Clicking on the Violentmonkey icon at any time will pop up a menu that shows you what userscripts are running on the website you are looking at. You can enable or disable each one by clicking on its name *(ticks mark enabled userscripts)*. The menu also lets you disable userscripts in general, or manage the scripts you have installed. -![Screenshot of Violentmonkey Dashboard][violentmonkeyChromiumScreenshot3] - +![Screenshot of Violentmonkey Dashboard][violentmonkeyBraveScreenshot3] Clicking "Open Dashboard" in the Violentmonkey menu takes you a dashboard screen that lists all your installed scripts. Each one can be temporarily disabled or removed totally using the buttons provided. You can also manually check for updated userscripts. ### Trouble shooting @@ -50,11 +49,12 @@ Sometimes, when you use more than one userscript on the same web page, they need [opera]: Opera [chrome]: Chrome [chromium]: Chromium +[brave]: Brave [firefox]: Firefox [gera2ld]: https://github.com/gera2ld [chromeAddons]: https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag -[chromeAddonsScreenshot1]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch1.gif "Violentmonkey in the Opera Add-ons website" +[braveAddonsScreenshot1]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_br1.gif "Violentmonkey in the Opera Add-ons website" [oujsScriptPageScreenshot]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/openuserjs_script.gif "Ready to install a script" -[violentmonkeyChromiumScreenshot2]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch3.gif "Installing a script" -[violentmonkeyChromiumScreenshot3]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch4.png "Violentmonkey Dashboard" +[violentmonkeyBraveScreenshot2]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_br3.gif "Installing a script" +[violentmonkeyBraveScreenshot3]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_br4.png "Violentmonkey Dashboard" [violentmonkeyIO]: https://violentmonkey.github.io/ diff --git a/views/includes/documents/Violentmonkey-for-Chrome.md b/views/includes/documents/Violentmonkey-for-Chrome.md deleted file mode 100644 index e67f56d13..000000000 --- a/views/includes/documents/Violentmonkey-for-Chrome.md +++ /dev/null @@ -1,60 +0,0 @@ -## Violentmonkey for Chrome - - -Violentmonkey is a userscript manager for the [Chrome][chrome], [Chromium][chromium], [Firefox][firefox], [Opera][opera] web browser, written by [gera2ld][gera2ld]. - -### Installing Violentmonkey - -To get userscripts going with the desktop version of Violentmonkey, first you have to install it from the [Chrome Add-ons website][chromeAddons]. - -![Screenshot of Violentmonkey page in Chrome Add-ons website][chromeAddonsScreenshot1] - -From the Violentmonkey page in the Chrome Add-ons website, click the green "Add to Chrome" button to install the extension. Once Violentmonkey has finished installing, you should see a pop-up confirming that Violentmonkey has been added to Chrome. This should point to a new Violentmonkey icon at the top of the Chrome window, next to the address bar. - -### Installing Userscripts - -Once Violentmonkey is installed, installing userscripts from [OpenUserJS.org][oujs] is simple. Navigate to the OpenUserJS page for the script, then click the blue "Install" button at the top of the page. - -![Screenshot of an OpenUserJS script page][oujsScriptPageScreenshot] - -Violentmonkey will display a screen showing you the source code of the userscript. Click the "Confirm installation" button to finish installing the script. - -![Screenshot of Violentmonkey script installation][violentmonkeyChromeScreenshot2] - -Installing userscripts from other sources is a similar process. You just need to find the installation link for the script. This will be a button or link to a file with a name that ends ".user.js" - -After installing a userscript, you won't normally notice any further changes until you visit, or refresh, a website that it runs on. - -### Managing Userscripts - -Clicking on the Violentmonkey icon at any time will pop up a menu that shows you what userscripts are running on the website you are looking at. You can enable or disable each one by clicking on its name *(ticks mark enabled userscripts)*. The menu also lets you disable userscripts in general, or manage the scripts you have installed. - -![Screenshot of Violentmonkey Dashboard][violentmonkeyChromeScreenshot3] - -Clicking "Open Dashboard" in the Violentmonkey menu takes you a dashboard screen that lists all your installed scripts. Each one can be temporarily disabled or removed totally using the buttons provided. You can also manually check for updated userscripts. - -### Trouble shooting - -If you think a userscript is causing problems, the easiest way to check is to switch off Violentmonkey, reload the web page, and see if the symptoms go away. You can do this by clicking on the Violentmonkey icon then clicking "Scripts enabled"; the tick next to the menu item should disappear and the monkey icon will turn grey. If it looks like a script problem and you have more than one script running on a web page, you can disable all of them in Violentmonkey's dashboard then re-enable them one by one until you find the culprit. Remember to reload the web page each time - userscripts normally only run when a web page loads. - -Sometimes, when you use more than one userscript on the same web page, they need to run in a particular order. You can change the order using the Violentmonkey dashboard. Click and drag the bounding box of each script in the list to move it up or down in the list. - -### More - -* [Get Violentmonkey from the Chrome Add-ons website][chromeAddons] -* [Violentmonkey IO][violentmonkeyIO] - some documentation for Violentmonkey. - -[githubFavicon]: https://assets-cdn.github.com/favicon.ico -[oujsFavicon]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/favicon16.png -[oujs]: https://openuserjs.org/ -[opera]: Opera -[chrome]: Chrome -[chromium]: Chromium -[firefox]: Firefox -[gera2ld]: https://github.com/gera2ld -[chromeAddons]: https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag -[chromeAddonsScreenshot1]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch1.gif "Violentmonkey in the Opera Add-ons website" -[oujsScriptPageScreenshot]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/openuserjs_script.gif "Ready to install a script" -[violentmonkeyChromeScreenshot2]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch3.gif "Installing a script" -[violentmonkeyChromeScreenshot3]: https://raw.githubusercontent.com/wiki/OpenUserJS/OpenUserJS.org/images/violentmonkey_ch4.png "Violentmonkey Dashboard" -[violentmonkeyIO]: https://violentmonkey.github.io/ diff --git a/views/includes/documents/Violentmonkey-for-Firefox.md b/views/includes/documents/Violentmonkey-for-Firefox.md index f38f438e0..5099e930d 100644 --- a/views/includes/documents/Violentmonkey-for-Firefox.md +++ b/views/includes/documents/Violentmonkey-for-Firefox.md @@ -1,5 +1,5 @@ ## Violentmonkey for Firefox - + Violentmonkey is a userscript manager for the [Firefox 52+][firefox], [Chrome][chrome], [Chromium][chromium], [Opera][opera] web browser, written by [gera2ld][gera2ld]. diff --git a/views/includes/documents/Violentmonkey-for-Opera.md b/views/includes/documents/Violentmonkey-for-Opera.md index 3baf516fd..146eaddb8 100644 --- a/views/includes/documents/Violentmonkey-for-Opera.md +++ b/views/includes/documents/Violentmonkey-for-Opera.md @@ -1,5 +1,5 @@ ## Violentmonkey for Opera - + Violentmonkey is a userscript manager for the [Opera][opera], [Chrome][chrome], [Chromium][chromium], [Firefox][firefox] web browser, written by [gera2ld][gera2ld]. diff --git a/views/includes/footer.html b/views/includes/footer.html index c6972f138..53b458972 100644 --- a/views/includes/footer.html +++ b/views/includes/footer.html @@ -3,7 +3,7 @@
{{/hideReminderGDPR}} {{/authedUser}} +{{#showInvalidAuth}} + +{{/showInvalidAuth}} +{{#showStratFail}} + +{{/showStratFail}} +{{#showNoConsent}} + +{{/showNoConsent}} +{{#showNoName}} + +{{/showNoName}} +{{#showTooLong}} + +{{/showTooLong}} +{{#showUsernameFail}} + +{{/showUsernameFail}} +{{#showROAuth}} + +{{/showROAuth}} +{{#showRetryAuth}} + +{{/showRetryAuth}} +{{#showAuthFail}} + +{{/showAuthFail}} {{#showReminderListLimit}} {{/showReminderListLimit}} +{{#showReminderInstallLimit}} + +{{/showReminderInstallLimit}} +{{#showSesssionNoExtend}} + +{{/showSesssionNoExtend}} +{{#showSessionMissingUsername}} + +{{/showSessionMissingUsername}} +{{#showSesssionCurrentSessionProhibited}} + +{{/showSesssionCurrentSessionProhibited}} +{{#showSesssionHigherRankProhibited}} + +{{/showSesssionHigherRankProhibited}} +{{#showSesssionNoOwned}} + +{{/showSesssionNoOwned}} +{{#showSesssionNoAdmin}} + +{{/showSesssionNoAdmin}} {{^hideReminderThis}} {{/isLib}} + {{> includes/header.html }} @@ -19,17 +20,15 @@
{{> includes/scriptPageHeader.html }} -
-
-

Description

-

Use GitHub Flavored Markdown for formatting.

-
- -
-
- -
-
+
+ +
+
+ + +
@@ -39,10 +38,10 @@

Description

{{^isLib}}
-
Script Groups
+
Script Groups
- +
{{/isLib}} diff --git a/views/pages/scriptIssueListPage.html b/views/pages/scriptIssueListPage.html index c117f5b71..88107b948 100644 --- a/views/pages/scriptIssueListPage.html +++ b/views/pages/scriptIssueListPage.html @@ -4,6 +4,7 @@ {{title}} {{> includes/head.html }} + {{> includes/header.html }} @@ -26,7 +27,7 @@ {{#script.support}} {{^isSameOrigin}} {{/isSameOrigin}} @@ -60,7 +61,7 @@ {{#script.contribution}}
@@ -58,7 +59,7 @@
diff --git a/views/pages/scriptPage.html b/views/pages/scriptPage.html index b9571e0b3..0346edac9 100644 --- a/views/pages/scriptPage.html +++ b/views/pages/scriptPage.html @@ -4,6 +4,7 @@ {{title}} {{> includes/head.html }} + {{> includes/header.html }} @@ -26,8 +27,8 @@ {{/script.isLib}}
-

Published:

-

Version: {{script.meta.UserScript.version.0.value}}+{{script.hashShort}}{{#script.isUpdated}} updated {{/script.isUpdated}}

+

Published:

+

Version: {{script.meta.UserScript.version.0.value}}+{{script.hashShort}}{{#script.isUpdated}} updated {{/script.isUpdated}}

{{#script.description}}

Summary: {{script.description}}

{{/script.description}} {{#script.hasGroups}} @@ -39,11 +40,17 @@ {{/script.hasGroups}} - {{#script.homepages}}

Homepage: {{text}}

{{/script.homepages}} - {{#script.support}}

Support: {{text}}

{{/script.support}} + {{#script.homepages}}

Homepage: {{text}}

{{/script.homepages}} + {{#script.support}}

Support: {{text}}

{{/script.support}} {{#script.copyrights}}

Copyright: {{name}}

{{/script.copyrights}} - {{#script.licenseConflict}}

License: {{#script.licenseParadox}}{{/script.licenseParadox}}MIT; https://opensource.org/licenses/MIT{{#script.licenseParadox}}{{/script.licenseParadox}}

{{/script.licenseConflict}} - {{#script.licenses}}

License: {{#name}}{{name}}{{/name}}{{^name}}{{spdx}}{{#url}}; {{url}}{{/url}}{{/name}}

{{/script.licenses}} + {{#script.licenseConflict}}

License: {{#script.licenseParadox}}{{/script.licenseParadox}}MIT; https://opensource.org/licenses/MIT{{#script.licenseParadox}}{{/script.licenseParadox}}

{{/script.licenseConflict}} + {{#script.licenses}}

License: {{#name}}{{name}}{{/name}}{{^name}}{{spdx}}{{#url}}; {{url}}{{/url}}{{/name}}

{{/script.licenses}} + {{^hasAntiFeature}} +

Antifeature: unspecified + {{/hasAntiFeature}} + {{#hasAntiFeature}} +

Antifeature: {{#script.antifeatures}} {{name}}{{/script.antifeatures}}

+ {{/hasAntiFeature}} {{#hasCollab}}

Collaborator: {{#script.collaborators}} {{text}} {{/script.collaborators}}

{{/hasCollab}} @@ -52,7 +59,7 @@ diff --git a/views/pages/scriptViewSourcePage.html b/views/pages/scriptViewSourcePage.html index 266a73469..2e04eb23d 100644 --- a/views/pages/scriptViewSourcePage.html +++ b/views/pages/scriptViewSourcePage.html @@ -18,6 +18,7 @@ color: #666; } + {{> includes/header.html }} @@ -37,17 +38,17 @@ {{^owner}}{{/owner}}
- - - - + + + +
{{/readOnly}} {{#readOnly}}
- - + +
{{/readOnly}}
@@ -60,11 +61,11 @@
{{> includes/footer.html }} {{> includes/scripts/lazyIconScript.html }} + {{> includes/scripts/scriptEditor.html }} {{> includes/scripts/clipboard.html }} {{#authorTools}} {{ > includes/scripts/selectInstall.html}} {{ > includes/scripts/selectSPDX.html}} {{/authorTools}} - {{> includes/scripts/scriptEditor.html }} diff --git a/views/pages/statusCodePage.html b/views/pages/statusCodePage.html index 093b3cada..fe2320410 100644 --- a/views/pages/statusCodePage.html +++ b/views/pages/statusCodePage.html @@ -21,11 +21,11 @@

{{statusCode}}

{{#isCustomView}} {{#statusData}} {{#isAdminNpmVersionView}} - + host v{{version}} - + {{/isAdminNpmVersionView}} {{#isAdminGitShortView}} @@ -45,7 +45,7 @@

{{statusCode}}

Session Length: {{length}} {{/isAdminSessionLengthView}} {{#isGHImport}} - {{{statusMessage}}}
{{utf_pathname}}{{utf_pathext}} + {{{statusMessage}}}
{{utf_pathname}}{{utf_pathext}} {{/isGHImport}} {{#isListView}} {{{statusMessage}}}
Please retry {{#retryAfter}}after approximately {{retryAfter}} seconds{{/retryAfter}}{{^retryAfter}}later{{/retryAfter}}. diff --git a/views/pages/userEditPreferencesPage.html b/views/pages/userEditPreferencesPage.html index 9daf487bf..db59ec969 100644 --- a/views/pages/userEditPreferencesPage.html +++ b/views/pages/userEditPreferencesPage.html @@ -21,7 +21,7 @@

Authentication

- {{defaultStrategy}} + {{defaultStrategy}} {{#defaultStrategyDisabled}} ᴿᴼ{{/defaultStrategyDisabled}}
@@ -36,7 +36,7 @@

Authentication

- {{display}} + {{display}}{{#disabled}} ᴿᴼ{{/disabled}}
@@ -54,7 +54,7 @@

Authentication

diff --git a/views/pages/userEditProfilePage.html b/views/pages/userEditProfilePage.html index fc48ace57..71b4bce0d 100644 --- a/views/pages/userEditProfilePage.html +++ b/views/pages/userEditProfilePage.html @@ -13,14 +13,16 @@
{{> includes/userPageHeader.html }} -
- -

Description

-

Use GitHub Flavored Markdown for formatting.

- + +
+ -
diff --git a/views/pages/userPage.html b/views/pages/userPage.html index ad887a6e8..21e92dc9f 100644 --- a/views/pages/userPage.html +++ b/views/pages/userPage.html @@ -15,7 +15,19 @@ {{#user.aboutRendered}}
-
{{{user.aboutRendered}}}
+ {{#authedUser}} +
{{{user.aboutRendered}}}
+ {{/authedUser}} + {{^authedUser}} + {{#user.isTrusted}} +
{{{user.aboutRendered}}}
+ {{/user.isTrusted}} + {{^user.isTrusted}} + {{#user.aboutRendered}} + This site requires you to be registered and logged in to view personal profiles. + {{/user.aboutRendered}} + {{/user.isTrusted}} + {{/authedUser}}
{{/user.aboutRendered}} diff --git a/views/pages/userSyncListPage.html b/views/pages/userSyncListPage.html new file mode 100644 index 000000000..ca1da18fc --- /dev/null +++ b/views/pages/userSyncListPage.html @@ -0,0 +1,42 @@ + + + + {{title}} + {{> includes/head.html }} + + + {{> includes/header.html }} +
+
+
+
+
+ {{> includes/userPageHeader.html }} + {{#paginationRendered}} +
+ {{{paginationRendered}}} +
+ {{/paginationRendered}} +
+ {{> includes/syncList.html }} +
+ {{#paginationRendered}} +
+ {{{paginationRendered}}} +
+ {{/paginationRendered}} +
+
+
+
+ {{> includes/searchBarPanel.html }} +
+
+
+ {{> includes/footer.html }} + {{#paginationRendered}} + {{> includes/scripts/showTopPagination.html }} + {{/paginationRendered}} + {{> includes/scripts/formControlClear.html }} + +