Compare commits

..
231 Commits
Author SHA1 Message Date
yu-i-i ae185bce23 Bib Editor: the initial commit (buggy) 2026-09-08 18:40:38 +02:00
yu-i-i 4156d6e1a6 Remove parasitic @misc{noauthor_notitle_nodate} entries 2026-08-04 16:12:07 +02:00
yu-i-i ac0789c565 Sandboxed compiles: fix cannot compile cyrillic 2026-08-04 10:25:02 +02:00
Rudy Ercek 00597dcd4a Fix: zip archive max uncompressed size 2026-08-04 10:07:10 +02:00
yu-i-i cf70bc751e Reference picker: fix show existing key as unexistent in Insert mode 2026-07-23 17:42:51 +02:00
yu-i-i 2e275a7489 Update README.md for 6.2.0 2026-07-17 18:07:23 +02:00
yu-i-i 27728c9283 Add syscalls to seccomp profile, fixes #194 2026-07-17 18:07:22 +02:00
yu-i-i 6c10182c60 Admin tools: improve Active Projects UI
- do not show the project owner, email and connections number columns
 - add mailto: all active users of a project when clicking the project title
 - fix error handling
2026-07-17 18:07:22 +02:00
yu-i-i c7f50c7de6 Fix: hide Git Bridge, GitHub Sync and Zotero UI elements when the corresponding feature is disabled 2026-07-17 18:07:22 +02:00
yu-i-i a547af1de3 Reference picker: improve usability
- show tags for nonexistent references
- click a badge to find the corresponding reference
2026-07-17 18:07:22 +02:00
yu-i-i febe214af6 Add variables to enable experimental features: pandoc conversion and editor tabs 2026-07-17 18:07:22 +02:00
yu-i-i aaea099e48 Registration page: the initial commit 2026-07-17 18:07:19 +02:00
yu-i-i fdeff3c174 Git bridge, Template gallery: remove integration with the old editor code 2026-07-17 18:03:42 +02:00
yu-i-i 652acede70 Admin tools: handle bad SMTP configuration error on account creation 2026-07-17 18:03:42 +02:00
yu-i-i c9eff55f05 Admin tools: remove themed pages env var; style admin header
- themed pages now is enabled by default upstream
  - make the header background red on admin manage urers/projects pages
  - fix minor bug
2026-07-17 18:03:42 +02:00
yu-i-i 9554d5b910 Github Sync: enhance token handling and post-sync indexing
- support file-based token cipher
  - reindex references after sync
  - fix typo and clean up code
2026-07-17 18:03:41 +02:00
yu-i-i 441d275948 Zotero: improve integration and reference import
- use OAuth instead of tokens
  - add an integration card
  - improve error handling
  - add BibLaTeX format support
  - fix indexing of linked .bib files on creation
2026-07-17 18:03:41 +02:00
yu-i-i c2f191b43c Linked URL: adapt for yarn; bump node to 24.14.1 2026-07-17 18:03:41 +02:00
David Rotermund 7f1c095039 Fix admin socket metadata handling and footer HTML rendering in admin-tools 2026-07-17 18:03:41 +02:00
Pierpaolo Vittorini 39971e0c30 Admin tools, Authentication: Fix: set analyticsId when creating a new user 2026-07-17 18:03:40 +02:00
yu-i-i 6478184ead Zotero: allow collaborators to refresh files imported from Zotero; show importer's name in the header 2026-07-17 18:03:40 +02:00
yu-i-i 35b6bdc582 Admin tools: move styles to modules directory; fix form for dark theme 2026-07-17 18:03:40 +02:00
yu-i-i fd583d5bf0 Github Sync: initial commit 2026-07-17 18:03:40 +02:00
yu-i-i 25765f6a7d Template Gallery: fix for 6.2.0 2026-07-17 18:03:39 +02:00
yu-i-i c133c1fdb8 Git integration: use named export for settings page widget
remove stray console.log
2026-07-17 18:03:39 +02:00
Synésio Neto 26683c3ba3 Fix a greyed out create file button after the Zotero Integration was merged. 2026-07-17 18:03:39 +02:00
yu-i-i 57a060c793 Sandboxed compiles: cwd argument was added to CommandRunner
see upstream commit ce6f9b8
2026-07-17 18:03:39 +02:00
yu-i-i cae6088557 Patches handled by yarn 2026-07-17 18:03:39 +02:00
yu-i-i bb025b29b2 Revert "[clsi] remove locking from docker actions (#32373)"
This reverts commit d66a856baa.
2026-07-17 18:03:39 +02:00
yu-i-i 6269f8e4ba Reference picker: normal search instead of fuzzy; overleafying modal 2026-07-17 18:03:39 +02:00
David Rotermund a83e54034f Reference picker 2026-07-17 18:03:36 +02:00
yu-i-i 5b4db4121b Authentication: fix ldap searchAttributes for passport-ldapauth 3.0.1 2026-07-17 18:01:58 +02:00
David Rotermund 463e4d5034 Zotero Integration (#164) 2026-07-17 18:01:56 +02:00
David Rotermund 629217a128 Logo Tools (#158)
* Logo Tools

* Moved to /tools/logo; Checks dependencies first and stops if they are not there; Added information about 3_remove_branding_from_projectpage.sh in README ; reordered the commands of 1_convert.sh to avoid the logo-horizontal.png problem
2026-07-17 18:00:24 +02:00
yu-i-i 621c554523 Git Bridge: fix incorrect token creation date display 2026-07-17 18:00:24 +02:00
David Rotermund 7b107886b4 Admin tools: introduce Activ Projects tab on the Manage Site page
- code moved to modules/admin-tools (yu-i-i)
2026-07-17 18:00:24 +02:00
yu-i-i 0f334b7932 Git Bridge: route PAT validation endpoint to correct router 2026-07-17 18:00:24 +02:00
yu-i-i d18da4336d Sandboxed compiles: avoid destroying containers of recently accessed projects
see upstream commit a9c4138
  here is an appropriate DockerRunner.mjs patch
2026-07-17 18:00:24 +02:00
yu-i-i dbdd5d4640 Admin tools: themed dashboard 2026-07-17 18:00:24 +02:00
Tao Chen 42f5226e09 Git Bridge: support git-bridge configuration via env in nginx
- generate overleaf.conf dynamically
- support GIT_BRIDGE_HOST and GIT_BRIDGE_PORT
- configure client_max_body_size for /git using GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE
2026-07-17 18:00:24 +02:00
yu-i-i 24d6518898 Linked URL: bump node to 24.13.0 2026-07-17 18:00:23 +02:00
yu-i-i f7b0755011 Git Bridge: integrations-panel code was moved 2026-07-17 18:00:23 +02:00
yu-i-i cee91bec80 Authentication: fix uid ldap login after email parsing was added to rate limiter 2026-07-17 18:00:23 +02:00
yu-i-i 9c409be818 Sandboxed compiles: convert DockerRunner to ESM 2026-07-17 18:00:23 +02:00
yu-i-i fdbeeeba5d Update README.md 2026-07-17 18:00:23 +02:00
yu-i-i 7317bd3e0c Sandboxed compiles: add scripts to add/strip repo prefix from texlive images 2026-07-17 18:00:23 +02:00
yu-i-i 9e0bc1e5a6 Add env variable to enable restoring project/file from history snapshot 2026-07-17 18:00:23 +02:00
yu-i-i a9b79d4a34 Admin tools: minor improvements
- enable Projects button in navbar for admin projects page
- enable copy of user ID in user info modal
- add null check in UserListController (thanks @moschlar)
2026-07-17 18:00:22 +02:00
yu-i-i 57fd623f64 Git Bridge: Add git integration 2026-07-17 18:00:22 +02:00
yu-i-i 7f1db9fdaf Sandboxed compiles: set imageName to default if undefined in project 2026-07-17 18:00:22 +02:00
yu-i-i 950cbe7ff1 Authentication: update passport-ldapauth to 3.0.1; fix minor issues 2026-07-17 18:00:22 +02:00
yu-i-i 5b6922d4bc Linked URL: prevent SSRF via DNS rebinding; minor fixes 2026-07-17 18:00:22 +02:00
yu-i-i 363e8b8f61 Template gallery: add template management authorization middleware
- Middleware to enforce template access rules
- Introduce template manager role
- Fix minor issues
2026-07-17 18:00:22 +02:00
yu-i-i 5142417020 Admin tools: optimize user/project list performance
Add frontend pagination
Add search debouncing
Render modals conditionally
Remove unnecessary sorting
2026-07-17 18:00:19 +02:00
yu-i-i 85859805e0 Update README.md 2026-07-17 17:59:21 +02:00
yu-i-i 1dab731bd8 Sandboxed Compiles: pass imageName in Copy project 2026-07-17 17:59:21 +02:00
yu-i-i 5c56d03656 Admin tools: fix minor bugs 2026-07-17 17:59:20 +02:00
yu-i-i 845bccfec5 Sandboxed compiles: add arm64 support to clsi seccomp profile 2026-07-17 17:59:20 +02:00
yu-i-i 412a1a539f Sandboxed Compiles: support configurable texlive image root via env var 2026-07-17 17:59:20 +02:00
yu-i-i e3175676c1 Admin Tools: Fix EXTERNAL_AUTH is 'none' 2026-07-17 17:59:20 +02:00
yu-i-i 76dfd21949 Admin Tools: Fix sorting 2026-07-17 17:59:20 +02:00
yu-i-i c03df586d6 Template Gallery: migration to 6.1.0
enable menu for editor redisign
move frontend code to modules
2026-07-17 17:59:20 +02:00
yu-i-i afaab641b1 Symbol Palette: fix for 6.1.0 2026-07-17 17:59:20 +02:00
yu-i-i 51454a08d7 Track changes: Fix import after upstream converted several files to ESM 2026-07-17 17:59:20 +02:00
yu-i-i 550272502e Authentication: Fix import after upstream converted some files to ESM 2026-07-17 17:59:19 +02:00
yu-i-i 4a87f36ef3 Admin Tools: Fix for 6.1.0 migraton 2026-07-17 17:59:19 +02:00
Guilhem Poy a25f82a606 Add support for EMAIL_SMTP_PASS_FILE
Small feature to provide the email password from a file rather than directly from the env variable OVERLEAF_EMAIL_SMTP_PASS. Useful when used in conjunction with Docker Compose secrets.
2026-07-17 17:59:19 +02:00
yu-i-i 5f6d248502 Admin Tools: Fix user name
fix user name is not updated

fix first_name/last_name is undefined
2026-07-17 17:59:19 +02:00
yu-i-i c6aab19393 Admin Tools: Manage users and Manage projects pages 2026-07-17 17:59:12 +02:00
yu-i-i b760ab7be1 Hotfix 5.5.6-ext-3.3 2026-07-17 17:56:50 +02:00
yu-i-i 42e7969f42 Linked URL: add sanitize and normalize 2026-07-17 17:56:49 +02:00
yu-i-i 7f1c0fdd4d README for extended CE 2026-07-17 17:56:49 +02:00
yu-i-i 18be9f31d2 Add service for Linked URL feature 2026-07-17 17:56:49 +02:00
yu-i-i ed65373e74 Sandboxed compiles: adjust clsi-profile 2026-07-17 17:56:49 +02:00
yu-i-i 15f99aa8c5 Track changes: Fix import after upstream converted DocumentUpdaterHandler and CollaboratorsGetter to ESM 2026-07-17 17:56:49 +02:00
yu-i-i 59984b59c0 Authentication: Fix import after upstream converted AuthenticationController to ESM 2026-07-17 17:56:49 +02:00
yu-i-i eeb61a3a2f Template gallery: Fix import after upstream converted AuthenticationController, ProjectGetter, ProjectLocator and DocumentUpdaterHandler` to ESM 2026-07-17 17:56:49 +02:00
yu-i-i 68bf77cbad Template gallery: Fix import after upstream converted CompileManager, ClsiManager and RateLimiterMiddleware to ESM 2026-07-17 17:56:48 +02:00
yu-i-i 9a511efdd8 Template gallery: Fix OLModal usage (see upstream commit 19b3834) 2026-07-17 17:56:48 +02:00
yu-i-i 41b78a87e0 Authentication: Fix import after upstream converted PermissionsManager, RateLimiterMiddleware, UserCreator and SAMLIdentityManager to ESM 2026-07-17 17:56:48 +02:00
yu-i-i 502eedffbd Track changes: Fix import after upstream converted ChatManager, UserInfoManager and UserInfoController to ESM 2026-07-17 17:56:48 +02:00
Rudy ERCEK a06480439b Add MAX_UPLOAD_SIZE environment variable 2026-07-17 17:56:48 +02:00
yu-i-i 7fce14beb7 Fix style of Templates button in navbar 2026-07-17 17:56:48 +02:00
yu-i-i f99a364148 Fix: comments thread does not necessarily belong to current document 2026-07-17 17:56:48 +02:00
yu-i-i 1bd06b6c4b Fix import after upstream converted AuthorizationMiddleware to ESM 2026-07-17 17:56:48 +02:00
yu-i-i 9f3236cc92 Track changes: Use getProjectRanges introduced in the upstream 2026-07-17 17:56:48 +02:00
yu-i-i 69b5ba751d Rename @node-saml/node-saml patch for v5.1.0 2026-07-17 17:56:48 +02:00
yu-i-i ad45c80c3a Fix imports after upstream converted UserController and CaptchaMiddleware to ESM 2026-07-17 17:56:47 +02:00
yu-i-i 43b195baa9 Fix import after upstream converted ErrorController to ESM 2026-07-17 17:56:47 +02:00
yu-i-i e3ebaca4b6 Bring back deleteFile removed upstream 2026-07-17 17:56:47 +02:00
yu-i-i 236ceb60de Symbol palette: ui components were moved to shared folder 2026-07-17 17:56:47 +02:00
yu-i-i 6fe766097b Move Sympol Palette code to modules, closes #63 2026-07-17 17:56:47 +02:00
yu-i-i acd3c256cd Template gallery: ui components were moved to shared folder 2026-07-17 17:56:47 +02:00
yu-i-i 6547ffbb46 passport-saml was updated to v5.1.0 2026-07-17 17:56:47 +02:00
yu-i-i 3fbae278e6 Fix: always include repository prefix in sandboxed compiles image name 2026-07-17 17:56:46 +02:00
yu-i-i d8fe1bd10c Fix template publishing to align with upstream project context changes 2026-07-17 17:56:46 +02:00
yu-i-i c704b9e04a Remove LDAP admin check via group search in ldap-passport 2026-07-17 17:56:46 +02:00
yu-i-i 637f66a91c toggleSymbolPalette now in useEditorPropertiesContext 2026-07-17 17:56:46 +02:00
yu-i-i 8a71e33174 Set use-ai capability to false 2026-07-17 17:56:46 +02:00
yu-i-i 7f9358f358 Track changes: convert to mjs 2026-07-17 17:56:46 +02:00
yu-i-i 00fa3a2082 Track changes: use getTrackedChangesUserIds introduced in the mainstream 2026-07-17 17:56:46 +02:00
yu-i-i 47fe923883 Remove OVERLEAF_OIDC_DISABLE_JIT_ACCOUNT_CREATION in favor of OVERLEAF_OIDC_ALLOWED_EMAIL_DOMAINS 2026-07-17 17:56:46 +02:00
Synésio Neto cc562f6314 Introduce an environment variable to allow JIT OIDC users creation based on their email address domain. 2026-07-17 17:56:46 +02:00
yu-i-i 71c76e10d6 Introduce an environment variable to control user creation in OIDC authentication, closes #47 2026-07-17 17:56:46 +02:00
David Rotermund fdaed06adc I need the --network=host settings for building the container 2026-07-17 17:56:45 +02:00
yu-i-i 371a736408 Avoid DEP0174 by removing async from callback-based getGroupPolicyForUser 2026-07-17 17:56:45 +02:00
yu-i-i b7c5088603 Fix login page 2026-07-17 17:56:45 +02:00
yu-i-i 07cf12a3ec Symbol Palette: make close button visible 2026-07-17 17:56:45 +02:00
yu-i-i 61591cd2e7 Template Gallery: replace markdown-it with marked 2026-07-17 17:56:45 +02:00
yu-i-i db1c746cc2 Refactor Template Gallery; resolves #38 and #39
- Replace free-text license input with a select box
- Improve visual presentation of modals and enhance keyboard interaction
2026-07-17 17:56:42 +02:00
yu-i-i 55e24c3158 Refactor Sandboxed Compiles 2026-07-17 17:56:08 +02:00
yu-i-i 90ca6f6666 Symbol Palette: improve keyboard input experience 2026-07-17 17:56:08 +02:00
yu-i-i 3c031dc7a0 Symbol Palette: get rid of @reach/tabs 2026-07-17 17:56:08 +02:00
yu-i-i baad2fabd4 Add 'poll' to clsi seccomp profile, fixes minted. Thanks, David. 2026-07-17 17:56:07 +02:00
yu-i-i bfd93d1071 Make Template Gallery optional; rename environment variables 2026-07-17 17:56:07 +02:00
yu-i-i 2a8b0bef67 Add Template Gallery support 2026-07-17 17:56:04 +02:00
yu-i-i e0050414d6 Track changes / comments: update backend to support frontend changes 2026-07-17 17:55:16 +02:00
yu-i-i 35dac5f76e Allow EXTERNAL_AUTH to be undefined, fixes #26 2026-07-17 17:55:16 +02:00
yu-i-i c9f28472fe Symbol palette: switch to 'OL' UI components and apply minor cosmetic changes 2026-07-17 17:55:16 +02:00
yu-i-i cc6ec7eb6e Make OVERLEAF_OIDC_USER_ID_FIELD support 'email' as a value 2026-07-17 17:55:15 +02:00
yu-i-i 72c739283e See upstream commit 42ee56e 2026-07-17 17:55:15 +02:00
yu-i-i edc66f8722 Fix glitches in symbol palette after switching to Bootstrap 5 2026-07-17 17:55:15 +02:00
yu-i-i e1c746aa68 Whitelist /oidc/login endpoint, fixes #21 2026-07-17 17:55:15 +02:00
yu-i-i 32ee132dd9 Add ENV variables to control SAML signature validation 2026-07-17 17:55:15 +02:00
yu-i-i 0325f4c519 Re-export doLogout (was removed from exports in commit b9fb636). 2026-07-17 17:55:15 +02:00
yu-i-i fae9cef1e4 Refactor authentication code; add OIDC support 2026-07-17 17:55:08 +02:00
yu-i-i 2b78561654 Allow adding extra flags to LaTeX compiler through environment variable 2026-07-17 17:54:27 +02:00
yu-i-i 5b8db1ddef Enable LDAP and SAML authentication support 2026-07-17 17:54:27 +02:00
yu-i-i d0f96f6587 Enable Symbol Palette 2026-07-17 17:54:19 +02:00
yu-i-i a6a32d6d58 Allow selecting a TeX Live image for a project 2026-07-17 17:52:29 +02:00
Sam Van den Vonder c8bb029509 Enable Sandboxed Compiles feature 2026-07-17 17:52:28 +02:00
yu-i-i c0c5bbd960 Enable track changes and comments feature 2026-07-17 17:52:28 +02:00
yu-i-i 1f6f83f5c3 Redirect non-existing links to Overleaf page 2026-07-17 17:52:28 +02:00
Kristina 28ad3b03b7 [web] add email notifications dry run split test (#34653)
GitOrigin-RevId: 62f73ce3850b994bf2a4a054edd6fa62134213a9
2026-06-26 08:08:18 +00:00
Alexandre Bourdin 33b8f27097 Merge pull request #34489 from overleaf/ab-keydown-event-logging
[web] Trigger full project search / focus mode on lowercase key matching for keyboard shortcuts

GitOrigin-RevId: a1786050c205ed46229b939c6325a3130c6fbcd3
2026-06-26 08:08:14 +00:00
Jakob Ackermann a6c98365d9 [web] use client-side filtering for split tests in admin panel (#34975)
* [web] use client-side filtering for split tests in admin panel

Filtering on non-indexed fields is not allowed with notablescan.

* [web] add initial value for DEFAULT_FILTERS.active

* [web] unset error before fetching split tests

GitOrigin-RevId: 0813873c5a90d57c4fdbfd0043fcc325f00408aa
2026-06-26 08:08:10 +00:00
Jakob Ackermann 1b553c0455 [migrations] only apply non-blocking migrations in CI (#34665)
* [migrations] only apply non-blocking migrations in CI

* [migrations] use "yarn run migrations" instead of "npm run migrations"

GitOrigin-RevId: d9f96db398adf2febcd82333295fb8d5eab741bb
2026-06-26 08:08:05 +00:00
Jakob Ackermann 9fd0d644cf [agents] refine guidance for running tests (#34977)
* [monorepo] fix handling of quoted MOCHA_GREP

* [agents] refine guidance for running tests

- nudge it towards scoped module targets
- nudge it to really run e2e tests when changed
- nudge it towards a11y cypress matchers
- recommend quoting for MOCHA_GREP

* [web] fix forwarding of mocha args from module Makefile

* [monorepo] use BRANCH_NAME_TAG_SAFE for building the ci image name

* [web] also make BRANCH_NAME_TAG_SAFE available in module Makefile

(It's not really needed, but will make copilot happy.)

GitOrigin-RevId: bfda3c8fe2b4f857ef111ac63f94b35a82ad75f8
2026-06-26 08:07:37 +00:00
Eric Mc Sween c645215afc Merge pull request #34907 from overleaf/em-promisify-github-sync
Promisify github-sync

GitOrigin-RevId: ac68f3f18ad211e204631d80bd5d4354ea23aaff
2026-06-26 08:07:30 +00:00
Eric Mc Sween 2c54574377 Merge pull request #34922 from overleaf/lg-vite-security-upgrade-v2
[Security upgrade] bump vite to 7.3.5 in root workspace

GitOrigin-RevId: 278475d98e23f0679ec45993e2a6816e9b93eb64
2026-06-26 08:07:25 +00:00
Eric Mc Sween 4fb8608908 Merge pull request #34884 from overleaf/em-already-in-your-library
Library import: flag entries that are already in the user's library

GitOrigin-RevId: f3c92fb59283631e1a9e78d2e08a3a365981df45
2026-06-26 08:07:21 +00:00
Mathias Jakobsen 75f7f82a3f Merge pull request #34923 from overleaf/mj-left-menu-field
[web] Clean up "left menu" concepts in new editor

GitOrigin-RevId: 9597c1c53af15f3a1587e62176bca36cd7c9069c
2026-06-26 08:07:10 +00:00
Mathias Jakobsen 5dd2c425ac Merge pull request #34912 from overleaf/mj-file-tree-style-cleanup
[web] Clean up file tree scss post editor redesign

GitOrigin-RevId: de2c5f3bd2979600897ffbbdcaead7ef72fa6ae1
2026-06-26 08:07:05 +00:00
Kate Crichton 9034e81c9b Merge pull request #34508 from overleaf/kc-email-admin-domain-reverification-failed
[web] email group admin when domain reverification failed

GitOrigin-RevId: d885f26b1b39184c25133f70024a72af66fa2d69
2026-06-26 08:07:01 +00:00
Kate Crichton 907f5a969a Merge pull request #34841 from overleaf/kc-grace-period-failedat-anchor
[web] Anchor domain-capture grace period on failedAt

GitOrigin-RevId: 5a31e5189512b22952c387722c7ad67f5c6397e3
2026-06-26 08:06:57 +00:00
Andrew Rumble bbf5586935 Merge pull request #34919 from overleaf/lg-undici-security-upgrade
[Security upgrade] bump undici to 7.28.0 in root workspace

GitOrigin-RevId: 39a6fb92235c6453fbd6106c05af66fb1d089607
2026-06-26 08:06:49 +00:00
Maria Florencia Besteiro Gonzalez 1e03046d68 Merge pull request #34587 from overleaf/lg-security-netty-codec-http2
[Security upgrade] bump io.netty:netty-codec-http2 to 4.1.135.Final in services/git-bridge

GitOrigin-RevId: e6b6b110bd4ce9c70c91f701cc3cb4f0868f870a
2026-06-25 08:06:26 +00:00
Jessica Lawshe 6c9b4aa498 Merge pull request #34768 from overleaf/jel-email-when-domain-verified
[web] Email group admin when domain is verified

GitOrigin-RevId: 5f8bc6187c561067d16863d3586a94ef498f3bdf
2026-06-25 08:06:22 +00:00
Jessica Lawshe 02d9c344f2 Merge pull request #34821 from overleaf/jel-clear-domain-notifications
[web] Make domain notifications dismissible

GitOrigin-RevId: fa1648efd0aa73213e5352b347a1b0f4c07dfc04
2026-06-25 08:06:18 +00:00
Jessica Lawshe 4a635d7599 Merge pull request #34819 from overleaf/jel-domain-table-spacing
[web] Update spacing in groups domain table

GitOrigin-RevId: 6e908b79e45212cf060068271ec56c3146fe20d7
2026-06-25 08:06:13 +00:00
Mathias Jakobsen a038e4b3a2 Merge pull request #34903 from overleaf/mj-settings-tabs-hover
[web] Add hover states to settings tabs

GitOrigin-RevId: c41c200231d0308267fc346d2949ef659a1950bc
2026-06-25 08:05:59 +00:00
Tim Down 3e29ce839f Merge pull request #34602 from overleaf/td-stripe-3ds1-redirect
Fix for Stripe redirecting to incorrect page

GitOrigin-RevId: ac90b2e501c4bf5f2738d61866ea72ba861e5539
2026-06-25 08:05:54 +00:00
ilkin-overleaf 13bc31cf25 Merge pull request #34870 from overleaf/ii-share-modal-group-sharing
[web] Make group project sharing available to Group Professional plans only

GitOrigin-RevId: ced66529818d573cfd20cef016ed4b3aafdd8d51
2026-06-25 08:05:50 +00:00
ilkin-overleaf f536b39b03 Merge pull request #34627 from overleaf/ii-share-modal-improvements
[web] New share modal improvements

GitOrigin-RevId: d8b47a6294f8d7e4833130fd2bb1720bfe2357d8
2026-06-25 08:05:45 +00:00
roo hutton 0fab15c58e Merge pull request #34730 from overleaf/rh-tax-id-verify
Validate tax ids and send emails on failed verification

GitOrigin-RevId: 84d53fdaf00d7b23ced22dd540a071e3028ced93
2026-06-25 08:05:34 +00:00
Brian Gough 29c8717b60 Merge pull request #33845 from overleaf/lg-ws-security-upgrade
[Security upgrade] pin ws@^8 to 8.21.0

GitOrigin-RevId: 40b839757122c1116debe8b7ebfa4eba9187bfa8
2026-06-24 08:06:51 +00:00
Andrew Rumble 255a6242cb Merge pull request #34340 from overleaf/ar-things-that-dont-need-to-be-pinned
[dependency-management] find unnecessary pins script

GitOrigin-RevId: b935ee7ae72c0339ea58f14d5f0a991ed4ae350c
2026-06-24 08:06:36 +00:00
Jakob Ackermann a8b4f2dc55 [web] reduce mongo lookups for compile requests (#34890)
* [web] _getUserCompileLimits: use owner.analyticsId directly

* [web] deduplicate project lookups for compile requests

* [web] add test case for prefetched project

GitOrigin-RevId: 0e140619c874accf8673153ed90af2ce70cf9234
2026-06-24 08:06:18 +00:00
Mathias Jakobsen 795ba4cd41 Merge pull request #34655 from overleaf/mj-ai-error-assistant-ide-redesign-cleanup
Clean up ai error assistant  style after editor redesign

GitOrigin-RevId: 8ca0747a567fe8f46b4d76886cd0723e1721ced1
2026-06-24 08:05:58 +00:00
Mathias Jakobsen fe52c3df12 Merge pull request #34872 from overleaf/mj-bulk-review-permissions
[web] Hide bulk track change actions for reviewers

GitOrigin-RevId: f636f4c82fdfb43a6dedf1abbb3cc53805eab7f8
2026-06-24 08:05:53 +00:00
Mathias Jakobsen c625ce428e Merge pull request #34875 from overleaf/mj-logs-link-colour
[web] Avoid default blue link colours in log headers

GitOrigin-RevId: 56359ededf5ba8c1a615bf1bf84ba6502f9c0078
2026-06-24 08:05:49 +00:00
David 7ce1e0bd48 Merge pull request #34797 from overleaf/dp-cleanup-reference-settings-migrated-field
[web] Add migration to unset reference settings migrated flag

GitOrigin-RevId: 570621d7a78f225a4bbf98f7046b096ffa735048
2026-06-24 08:05:38 +00:00
Brian Gough c6a46fbdad Merge pull request #33961 from overleaf/lg-qs-security-upgrade
[Security upgrade] pin qs to 6.15.2 in root yarn.lock

GitOrigin-RevId: 4819f0a1f9b41821ccda354a309d190d8395ef56
2026-06-24 08:05:31 +00:00
Brian Gough 81f85293f2 Merge pull request #33656 from overleaf/lg-protobufjs-security-upgrade
[Security upgrade] Pin protobufjs to 7.6.3 and @protobufjs/utf8 to 1.1.1 via resolutions

GitOrigin-RevId: 7d71d564d7296af76f04fda8b35f178c5014a9b9
2026-06-24 08:05:26 +00:00
Jakob Ackermann 1cc18dd0f0 [web] avoid UserAnalyticsIdCache for split-test assignments (#34803)
* [web] split-tests: add helpers to using an already fetched mongo user

* [web] split-tests: add ignoreOverrides option to getAssignment

* [web] split-tests: reuse already fetched mongo users where possible

* [web] notifications: fetch users in bulk for split-test assignment

* [web] split-tests: use session for assignment where possible

* [web] split-tests: pass req/res down into domain-capture hook

* [web] split-tests: explicitly throw early

* [web] split-tests: refine jsdoc types

* [web] mark analyticsId and labsProgram as required in user model

* [web] split-tests: refine jsdoc types

* Revert "[web] split-tests: pass req/res down into domain-capture hook"

This reverts commit 897a158384cd2b3e416fc34495b20d1e2a0e68f0.

GitOrigin-RevId: ea4afd15c2506f369827b5193fd16d220d3c7d0e
2026-06-23 08:06:42 +00:00
Jakob Ackermann 6b084424b5 [monorepo] fix running make lint from root (#33559)
* [monorepo] remove spurious package-name field from buildscript.txt

* [monorepo] align variables/paths across build script variants

* [monorepo] align linting/test setup for jobs/libraries

* [monorepo] fix "make lint/lint_fix" from root

* [wiki-algolia] integrate with yarn workspace

GitOrigin-RevId: a0954f2936d03cd4a69b3f2c8098d9ee2e361993
2026-06-23 08:06:37 +00:00
Eric Mc Sween 006b30c441 Merge pull request #34817 from overleaf/em-library-backend-search-2
Library search: make backend search tokens consistent with frontend (re-apply #34643)

GitOrigin-RevId: e5a143edf3226c12eaff77b2bc4cdb68e139bb10
2026-06-23 08:06:33 +00:00
Jimmy Domagala-Tang 5cd4447cbf Adding 2026 TeXLive behind admin (#34291)
* feat: adding 2026 behind admin

* feat: bumping packer and clsi memory to accomodate new image

* align packer build and packer verify disk sizes

* feat: adding more space for packer build

GitOrigin-RevId: 1961ea7937e2c751137a4b9564a34d4e371ce0f9
2026-06-23 08:06:14 +00:00
Jakob Ackermann 88267716a3 [web] adopt free/premium compile backend class (#34846)
* [web] adopt free/premium compile backend class

* [rails] adopt free compile backend class for pdf conversion

GitOrigin-RevId: e12c99a27363f5654132c2fcdc6c5d8ccc591868
2026-06-23 08:05:59 +00:00
Brian Gough 23dca72cd3 Merge pull request #34843 from overleaf/bg-add-compile-timer
add timer for compile with checkpoints

GitOrigin-RevId: ec758d34594f242a5cac8192233bfe2db783e42a
2026-06-23 08:05:55 +00:00
Brian Gough b1ed38c2d0 Merge pull request #34761 from overleaf/bg-enable-clsi-checkpoints
adds enableCheckpoint to compile requests from web to clsi

GitOrigin-RevId: 431b28602091cf8f7252ad836f083c7b092351d4
2026-06-23 08:05:50 +00:00
Jakob Ackermann 3be2f320f8 [web] fix preference of new/old clsiServerId in double-compiles (#34862)
GitOrigin-RevId: 81cbbcec9fef79ebd0c5e7d90048fc4bda302108
2026-06-23 08:05:42 +00:00
Brian Gough d75ef19bbf Merge pull request #34746 from overleaf/bg-clsi-apparmor
add new clsi apparmor profile for checkpoints

GitOrigin-RevId: 3a003263326613d3166343d54adb7050fe1e9847
2026-06-23 08:05:38 +00:00
Mathias Jakobsen e0cfb07f60 Merge pull request #34698 from overleaf/mj-context-menu-tracked-changes
[web] Add tracked changes options to context menu

GitOrigin-RevId: b49198c6c2c63063d96ace69f10b617ac819bb04
2026-06-23 08:05:33 +00:00
Kristina a53b164b45 [web] email content previews for notification emails (#34576)
* add content previews to notification emails
* add fetcher for doc lines
* gate content previews behind a split test
* make composable blocks for rendering

GitOrigin-RevId: c8df384d55a301084b98df76fd3edd6dde6ea50c
2026-06-22 08:06:25 +00:00
Domagoj Kriskovic 63a605484c Add support for rendering math in Markdown with MathJax
GitOrigin-RevId: dd1c1c72237a5364c642e46c66e08ea7a6977662
2026-06-22 08:05:40 +00:00
Mathias Jakobsen ef05288f81 Merge pull request #34714 from overleaf/mj-tracked-change-merges
[overleaf-editor-core+ranges-tracker] Pick lower timestamp when merging tracking info

GitOrigin-RevId: 5b1c8d7c719003df4506014c381f2a525d0a7ff4
2026-06-19 08:07:27 +00:00
Eric Mc Sween 7ee385fe45 Merge pull request #34816 from overleaf/revert-34643-em-library-backend-search
Revert "Library search: make backend search tokens consistent with frontend (follow-up to #34386)"

GitOrigin-RevId: 83328f2ee4a9c3f668325fa4de1ca652e7c718bf
2026-06-19 08:07:19 +00:00
Eric Mc Sween 80218df85e Merge pull request #34643 from overleaf/em-library-backend-search
Library search: make backend search tokens consistent with frontend (follow-up to #34386)

GitOrigin-RevId: 91ef5b5dc81ba8ebda43e746c9b747b7636f3e80
2026-06-19 08:07:12 +00:00
Eric Mc Sween 0371ba46f6 Merge pull request #34693 from overleaf/em-upload-bib
[web] Add "Upload .bib file" option to library

GitOrigin-RevId: 7067a4cb10f0973c0423507836ef13555329942c
2026-06-19 08:07:08 +00:00
Kate Crichton 0883e0fec0 Merge pull request #34513 from overleaf/kc-verify-domain-from-table
[web] verify domain from table

GitOrigin-RevId: 2e41f76be26b81f204208fab714f2e3399071499
2026-06-19 08:06:34 +00:00
Kate Crichton f7b1ccae9a Merge pull request #33852 from overleaf/kc-schedule-domain-verification
[web] schedule domain reverification

GitOrigin-RevId: 08217692960379d0a6f7ce457032b1182fb2bf4e
2026-06-19 08:06:30 +00:00
Liam O'BrienandClaude Opus 4.8 bc9c85019f Lo zotero initial library sync (#34456)
* Migration and index setup script

* Helper script to clear linked library

* Pull users collections

* Implementing get items from users zotero library

* Initial zotero sync client

* Pulling users overleaf collection

* Pulling users overleaf collection

* Initial sync pull from Zotero to Library

* Sync and import workflow from Zotero into Library

* Using user getter and setter for updating user library info

* Using new collection for library sync states

* Adding tests for new sync functionality

* Harden Zotero sync route

* Simplify Zotero sync state read and skip accounting

Fold the ensure+get double round-trip in sync-state discovery into one
findOneAndUpdate, and derive itemsSkipped from items.length - itemsInserted
instead of a running counter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Redact URL-encoded token secret from OAuth1 request logs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fixing type failure

* Rename upsertZoteroReferences to importZoteroReferences

The method is insert-only; the bulkWrite upsert just makes re-runs
idempotent rather than updating, so the name now reflects that.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Validate librarySyncStates reads with zod

Replace the `as` / `as unknown as` casts in getLibrarySyncState and
ensureLibrarySyncState with a zod schema parse, matching
LibraryReferenceRepository. Types are now derived from the schema.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Defer Zotero sync watermark until pagination exists

Don't record lastSyncedVersion after a single-page sync: committing the
page-1 version before pagination (#34286) lands would let incremental
sync (#34282) skip items beyond the first page. Drops the commitSyncState
call and toVersion from SyncResult; the version envelope is left for the
follow-ups. commitSyncState stays in the repository for #34282.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Extract OAuth1Transport from OAuth1Handler

Lift the OAuth1 client/config registry, getAccessToken, redactSecret and
classifyOAuth1Error into oauth1/OAuth1Transport.js, plus a signedGet
helper for provider modules to reuse. OAuth1Handler now imports them; its
method bodies are unchanged. Test mocks move to esmock's global scope so
they reach the transport, with Errors pinned for instanceof checks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Move Zotero collections/items into a per-provider ZoteroProvider

Carve the Zotero-only operations out of the generic OAuth1 handler into
providers/ZoteroProvider.js, dispatched via providers/ProviderRegistry.js.
ReferenceProviderHandler now resolves the provider from the registry and
throws UnsupportedOperationError (mapped to 501) for providers without the
operation, instead of bolting Zotero-only methods onto OAuth1Handler.

Zotero's collectionsUri/itemsTopUri are read from settings by ZoteroProvider
rather than copied into the shared OAuth1 registry. Removes getCollections/
getUsersItemsChanges (and their tests) from OAuth1Handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add ZoteroProvider unit tests and unsupported-provider coverage

Cover ZoteroProvider collections/items parsing, header envelope and
malformed-payload rejection, and assert ReferenceProviderHandler throws
UnsupportedOperationError for providers without a sync implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Make addSyncedCollection idempotent

Only push a synced collection when its key isn't already recorded, so a
repeat sync can't create duplicate syncedCollections entries regardless of
the caller-side guard. Adds a sequential test asserting no duplicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Hardcode Zotero library endpoint paths, derive host from site

Move the collections/items-top endpoints out of settings and into
ZoteroProvider as path constants, taking only the host from the
provider's `site`. Aligns Zotero `site` with the other providers
(API host) and avoids adding per-endpoint config points.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Make Zotero sync routes explicit, 404 unknown providers

Hardcode the collections/items routes to /zotero rather than a generic
/:ref_provider, so a non-Zotero request 404s on the missing route
instead of returning a misleading 501. Drop the UnsupportedOperationError
501 branch, reverting OAuth2 unsupported ops to their prior 500.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: 50e16a9c46f995c2e9db6eed50ef81c3a71ea71d
2026-06-19 08:06:07 +00:00
Antoine ClausseandClaude Opus 4.8 e5f712a857 [web] Cleanup plans-2026-phase-1 - phase 2: pug/backend pricing (#34623)
* Remove `plans2026` from pug files in subscriptions module

* Remove `plans2026` param from `makeGroupPlans`

* Remove `plans2026` param from `makeUISettings` in interstitial-payment.mjs

* Remove `plans2026` param from `makeInterstitialPaymentConfig` in interstitial-payment.mjs

* Remove `plans2026` param from `makeIndividualPlans`

* Remove `plans2026` param from `makeStudentPlans`

* Remove `plans2026` from try-premium page

* Remove `plans2026` from controllers

* Remove unused files

* Update translations

* Update tests

* Update tests

* Remove unused translations

* Rename the collaborator upgrade-mode test to read explicitly as the
Standard-plan replacement for the removed redirect test, and drop the
stale "when plans 2026 is enabled" framing now the controller no longer
branches on the flag.

* Surface controller errors in InterstitialPayment tests

Move assertions out of the res.render callback into render/redirect
helpers that resolve with the captured args and reject on the wrong
path or a controller rejection, so failures show the real error
instead of timing out. Restore the ai-limit-upgrade-copy split-test
assignment for the AI copy tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* format

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
GitOrigin-RevId: f0b055b4d92b494b531766a13b057336d9c2c9af
2026-06-19 08:06:03 +00:00
David d6e55f81ff Merge pull request #34750 from overleaf/dp-cleanup-reference-settings-migration
[web] Remove reference manager settings migration code

GitOrigin-RevId: 1062e1f08a6e6d4547f48c965c342d2ac15e9a4f
2026-06-19 08:05:56 +00:00
Copilot ea1871b0df Use Ctrl+Shift+C for the add-comment shortcut while keeping Cmd+Shift+C active (#32624)
GitOrigin-RevId: 5fdf9ae7dd0fa194c3ac6acf9abb70ac9721fd2e
2026-06-19 08:05:48 +00:00
Mathias Jakobsen b570078223 Merge pull request #34774 from overleaf/mj-fix-cleanup-locales
[web] Ignore cache files in cleanupUnusedLocales

GitOrigin-RevId: 72ad8fb265a6fe905671dd379b3ca29468c9f759
2026-06-19 08:05:10 +00:00
Mathias Jakobsen 5f79c2dd10 Merge pull request #34512 from overleaf/mj-floating-toolbar-option
[web] Add setting for floating toolbar menu

GitOrigin-RevId: bd28f1f287f2011535c7fd530f1217f01e0cfbbc
2026-06-18 08:07:33 +00:00
Jessica Lawshe 106dd1b07b Merge pull request #34702 from overleaf/jel-grace-period-ui
[web] Update domain verification failed grace period UI

GitOrigin-RevId: 267dc4e4cc60b959faaacecc5a13b662d670e313
2026-06-18 08:07:09 +00:00
MoxAmber a3036730b4 Merge pull request #34644 from overleaf/as-link-sharing-analytics
[web] Add sharing link analytics events

GitOrigin-RevId: b206732397065c399d38a8e42dbdebe60222fb24
2026-06-18 08:06:52 +00:00
MoxAmber ad9b28af18 Merge pull request #34537 from overleaf/ii-share-modal-invited-people
Show "No one invited yet" instead of "1 person invited" for solo projects

GitOrigin-RevId: 0423b507e0574dc5a381cbe4044912a25af7a3f0
2026-06-18 08:06:48 +00:00
MoxAmber a7a3ccdb71 Merge pull request #34639 from overleaf/as-link-sharing-editinvitecount
[web] Ignore reuable invites (aka sharing links) when checking edit invite count

GitOrigin-RevId: 355ec6e6b2d5d497392fe1bdc04e68ad80349518
2026-06-18 08:06:44 +00:00
MoxAmber bd8a156908 Merge pull request #34394 from overleaf/as-link-sharing-groups-split-test
[web] Move group link sharing behind a separate split test

GitOrigin-RevId: c973a9bccb88f427a9f51d121b58a8799993e7c9
2026-06-18 08:06:39 +00:00
Domagoj Kriskovic 62c2523aec Add table support for visual markdown editor
GitOrigin-RevId: b36f575d40b19fdf32622c9923cecd5b47658d5d
2026-06-18 08:06:26 +00:00
Eric Mc Sween 95e0a88bbb Merge pull request #34519 from overleaf/em-github-sync-octokit-migration
Replace octonode with octokit in github-sync

GitOrigin-RevId: 05886b87ed8625dbbcb77dc2d6ccbeda7b426e79
2026-06-18 08:06:22 +00:00
roo hutton 1dfe6e60b3 Merge pull request #34534 from overleaf/rh-rm-recurly
[web] Remove conditional Recurly checkout page

GitOrigin-RevId: 5bb773080bba682628f0f5428c72b7dff75b6015
2026-06-18 08:06:17 +00:00
Davinder SinghandCopilot Autofix powered by AI 6613ef2f18 [WEB] Move Review Toggle into the toolbar - 2 (#34578)
* wip of adding Review Toggle into the toolbar

* renaming to isToolbarMigration

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* removing extra comments

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
GitOrigin-RevId: 2c0b54ae983a32c083def8e9e64ddd0fb7ddf334
2026-06-18 08:06:10 +00:00
Malik Glossop b04fea4726 Merge pull request #34559 from overleaf/mg-writefull-workbench
Allow opening workbench and hide texgpt for ai menu behind flag

GitOrigin-RevId: f4de64ce9b68f4e337772e164e46e27d2a41a0e7
2026-06-18 08:06:06 +00:00
Antoine Clausse 0d004926e6 [web] Cleanup plans-2026-phase-1 - phase 1: frontend (#34619)
* Remove `plans-2026-phase-1` from frontend in subscriptions module

* Remove `plans-2026-phase-1` from general frontend

* Remove empty `AiErrorAssistantPaywall`

* Simplify `UpgradeNotification`

* Update translations

* Fix tests

* Remove `OverleafAssistPaywall`

* Remove unused locales

GitOrigin-RevId: 4cf04bbde134a3f7b778a0695d388f31d9765343
2026-06-18 08:06:01 +00:00
Mathias Jakobsen fb9ebfcc10 Merge pull request #34459 from overleaf/mj-review-panel-style-cleanup
[web] Clean up review styles post-editor redesign

GitOrigin-RevId: b8b535f765f446770e62e31519f7f1e98dc7e3d2
2026-06-18 08:05:54 +00:00
Mathias Jakobsen a14971b790 Merge pull request #34600 from overleaf/mj-collapsible-file-header-cleanup
[web] Clean up collapsible file header styles post editor redesign

GitOrigin-RevId: a5c8749360433213d6602d029ea6b66312c9032b
2026-06-18 08:05:49 +00:00
Rebeka Dekany 51fc266bc2 Redirect users without an active subscription to the subscription page instead of the error page (#34637)
* Redirect freemium users from billing portal routes to subscription page with banner

* Handle null management link and unknown service in subscription redirects

* Move deleted-subscription invoices link inside `getCustomerManagementLink` instead

GitOrigin-RevId: 7122f69e9b8bda6238111f76c15a35135ce5f0ac
2026-06-18 08:05:22 +00:00
Jakob AckermannandAndrew Rumble 6323fddbd8 [web] avoid UserAnalyticsIdCache (#33121)
* [web] always provide an analyticsId when registering new users

* [web] add recordEventForMongoUser and use it during login

* [web] use recordEventForMongoUser/recordEventForSession where possible

* [web] use session cache for analyticsId when refreshing features

* [web] add setUserPropertyForMongoUser and use it where possible

* [web] use setUserPropertyForSessionInBackground where possible

* [web] use mongo user for analyticsId when setting utm user properties

* [web] admin-panel: user updated user when sending events

* [web] enforce labsProgram being populated on user object

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>

* [web] revert FeaturesUpdater.refreshFeatures change

* [web] labs: move session maintenance into handler

* [web] analyticsMiddleware: ensure that all users have labsProgram field

* [web] migrate a few more events to recordEventForSession

* [migrations] mark back-fill of db.users.analyticsId as required

* [web] setUserPropertyForSessionInBackground: throw early

---------

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
GitOrigin-RevId: 19c26bc36abd8c11f0a8f458d25abfb7cbda0bb7
2026-06-17 08:08:32 +00:00
Anna Claire Fields 7035a2e025 Revert "Acf pnp migration master" (#34709)
GitOrigin-RevId: ec7d63e1212e05dd6dfc7a0ddf785c11eca76ad6
2026-06-17 08:07:56 +00:00
Anna Claire Fields e1226515f6 PNP deployment: clsi-lb, web, latexqc fixes (#34696)
GitOrigin-RevId: fe1f5a7670d6fcf2128c5e2b50cc2a544afb1ebc
2026-06-17 08:07:36 +00:00
Anna Claire Fields 8f07308342 Acf pnp migration master (#34040)
GitOrigin-RevId: 418eab9bc733f27a6e0f8f04a2a8e21c2f5a491f
2026-06-17 08:07:16 +00:00
Jakob Ackermann d44e862a0c [web] throw early in UserCreator.createNewUser with missing analyticsId (#34664)
* [web] throw early in UserCreator.createNewUser with missing analyticsId

* [migrations] rerun back-fill of db.users.analyticsId

GitOrigin-RevId: eabf16326192fca4dbea00ad643cee4315c426c6
2026-06-17 08:06:59 +00:00
Jakob Ackermann 6bc9f1dae4 [server-ce] test: the first startWith must resetData when using dataDir (#34671)
* [server-ce] test: the first startWith must resetData when using dataDir

* [server-ce] test: fix order of creating user and resetting DB

GitOrigin-RevId: 2a6e6344919fe48d41704e1a3cb39c6e22b5a924
2026-06-17 08:06:54 +00:00
Malik Glossop bf22bf9eed Merge pull request #34487 from overleaf/mg-fix-nested-context-menu
Block native context menu on editor context menu and fix focus styling

GitOrigin-RevId: c2d59e7acc59019b5e2777cc1bf3ad24a7d8e54e
2026-06-17 08:06:49 +00:00
Malik Glossop 8ef463f8b0 Merge pull request #34357 from overleaf/mg-combine-floating-menus
Add combined floating menu for comment, tracked changes, writefull

GitOrigin-RevId: 4de5b9d3b1717fc76394f7c0edfe4d7230394c8d
2026-06-17 08:06:44 +00:00
David 9fae908602 Merge pull request #34585 from overleaf/lg-security-hapi-wreck-joi
[Security upgrade] bump @hapi/wreck from 18.1.0 to 18.1.2 in services/third-party-references

GitOrigin-RevId: a6efbbb9b7c8ddf995c247f8b787c16fa8b6d6f3
2026-06-17 08:06:34 +00:00
Davinder Singh 8e8804a318 [WEB] Add TeXGPT button to the main toolbar (#34219)
* feat: add writefull-toolbar-migration split test and
  sourceEditorToolbarStartButtons slot

* feat: add TeXGPT button to main editor toolbar (writefull-toolbar-migration)

* removing wrapper div

* hide toolbar start buttons from readonly users

GitOrigin-RevId: 6bafe3ad3fade98c318ea0d0b2714957c1d2d9b4
2026-06-17 08:06:16 +00:00
Davinder Singh e1881a7645 [WEB] Toolbar overflow: drop one button at a time (#34171)
* adding individual hiding logic for the toolbar icons

* using css for logic

GitOrigin-RevId: 87e9f72c1dab888e2701ef108ecaa0b190e366ff
2026-06-17 08:06:11 +00:00
CopilotandJakob Ackermann f40cebf60b [docstore] replace remaining Joi usage with zod (#34603)
Co-authored-by: Jakob Ackermann <jakob.ackermann@overleaf.com>
GitOrigin-RevId: 41ca0c35d59bf6accd619f6b5149be14a69e1078
2026-06-16 08:07:22 +00:00
Brian Gough 6f9b7031f4 pass through checkpoint setting (#34631)
GitOrigin-RevId: 50e9791b468253ee966b0eac822b2a01321a0ece
2026-06-16 08:07:14 +00:00
Brian Gough be4108a573 add glob matching to isExtraneousFile (#34633)
GitOrigin-RevId: f12aedb52195cfc9e3254936f1db6189f49c3f37
2026-06-16 08:07:09 +00:00
Domagoj Kriskovic 0778a0e4d3 Revert "Add markdown link tooltip support to the visual editor"
This reverts commit 7ab4fd0793f057c86e4fc0307d81df376efe4d34.

GitOrigin-RevId: 625a5cbbb39832f6a32a26d2ad454669d0ed1846
2026-06-16 08:07:01 +00:00
Rebeka DekanyandM Fahru 1f28eaecdb [web] Add Stripe acceptance tests (#34577)
* Add StripeClient mock-target support and Stripe test settings

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add Stripe payment page acceptance tests

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add MockStripeApi for stateful Stripe acceptance tests

Co-Authored-By: M Fahru <m.fahru@overleaf.com>

* Add Stripe subscription acceptance tests

* Add Stripe webhook acceptance tests

* Use a helper for setting the subscription status

---------

Co-authored-by: M Fahru <m.fahru@overleaf.com>
GitOrigin-RevId: b9bc50c914369da646ddb42ade1a02fd7baeb96b
2026-06-16 08:06:57 +00:00
Jakob AckermannandAndrew Rumble f2ebe46506 [web] add migration for back-filling db.users.labsProgram=false (#34632)
* [web] add migration for back-filling db.users.labsProgram=false

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>

* [web] include user.labsProgram=false in session

---------

Co-authored-by: Andrew Rumble <andrew.rumble@overleaf.com>
GitOrigin-RevId: 0b3322007f6a36d2999d646869dc84f2c6ad4d2d
2026-06-16 08:06:43 +00:00
Jakob Ackermann cb9e834a1f [project-history] return status "423 Locked" when redis lock is taken (#34429)
GitOrigin-RevId: f40230bc98e8ed8a5358e6db5c515cb2a58f9b85
2026-06-16 08:06:38 +00:00
Jakob Ackermann b11027d58f [monorepo] add clsi-sharded mode (#34614)
* [monorepo] provide clsi-sharded dev-env setup

* [monorepo] provide sharded clsi-nginx when using clsi-sharded

* [clsi-perf] run on all instances again

* [monorepo] switch default TL to 2025.1 in clsi (same as web)

* [monorepo] avoid starting base clsi when running clsi-perf with clsi-sharded

* [clsi-lb] use 3s check interval in clsi-sharded mode

GitOrigin-RevId: 3cae8f0c1db69dbd76b94b45dc45b3abb2fa0a83
2026-06-16 08:06:34 +00:00
Andrew Rumble d22cea01ce Merge pull request #33404 from overleaf/copilot/fix-accepts-language-deprecation
Fix deprecated req.acceptsLanguage → req.acceptsLanguages in Translations.mjs

GitOrigin-RevId: db9f09d999158ba5f9f084bc1a8334954a55cd79
2026-06-16 08:06:29 +00:00
Mathias Jakobsen b7946791f4 Merge pull request #34553 from overleaf/mj-pandoc-html-mathml
[clsi] Add mathml option to HTML export

GitOrigin-RevId: c0736f0ae78020c2c554e8aaed9472f050323fd4
2026-06-16 08:06:25 +00:00
Mathias Jakobsen 481a0b1da9 Merge pull request #34479 from overleaf/mj-toolbar-border
[web] Hide toolbar border when tabs are enabled

GitOrigin-RevId: 58a33980006d1bc148db16d864fc0547d9c5d421
2026-06-16 08:06:21 +00:00
Mathias Jakobsen 7096b0f8a3 Merge pull request #34496 from overleaf/mj-history-style-cleanup
[web] Clean up history styles post-editor redesign

GitOrigin-RevId: fffdc0074000f6076984b35e3f84467e4d881c09
2026-06-16 08:06:16 +00:00
Mathias Jakobsen b85702f169 Merge pull request #34592 from overleaf/mj-full-project-search-redesign-cleanup
[web] Clean up full-project-search styles post-editor redesign

GitOrigin-RevId: a93660b48f8b088244f5d99db41752402032953f
2026-06-16 08:06:12 +00:00
Mathias Jakobsen 69ec7cdeb6 Merge pull request #34575 from overleaf/mj-file-view-cleanup
[web] Clean up file view styles post editor redesign

GitOrigin-RevId: 702200b1dc0ff6dee44e9163cf9ee3540ad1f621
2026-06-16 08:06:07 +00:00
Mathias Jakobsen b7f984fc8d [web] View menu tweaks (#34548)
* [web] Add missing translation

* [web] Group PDF zoom options in view menu

* [web] Add review panel toggle to view menu

* [web] Address review comments

* [web] Show disabled buttons to anonymous users

* [web] Add e2e tests for editing mode in view menu

GitOrigin-RevId: d13dc307f1bcb7c50c76afbaa397c3aca8b96029
2026-06-16 08:06:03 +00:00
roo hutton efd12c8feb Merge pull request #34484 from overleaf/rh-ol-stripe-id
Sync updated customer email to Stripe/Recurly

GitOrigin-RevId: b2becc50cc63739bdf56e45c37939f84c6fcd7a8
2026-06-16 08:05:58 +00:00
Lucie Germain ed166a7291 [Security upgrade] upgrade react-router to 6.30.4 in latexqc (#34324)
* Upgrade react-router to 6.30.4 (GHSA-2j2x-hqr9-3h42)

GitOrigin-RevId: 1224dc24bac65a8d2ee2d7c3f83d900a22aae5a0
2026-06-16 08:05:54 +00:00
Domagoj Kriskovic 8ccdc0fbc4 Add markdown link tooltip support to the visual editor
GitOrigin-RevId: 7ab4fd0793f057c86e4fc0307d81df376efe4d34
2026-06-16 08:05:46 +00:00
808 changed files with 67557 additions and 2766 deletions
+2 -6
View File
@@ -8,12 +8,8 @@ assignees: ''
<!-- <!--
Note: If you are using www.overleaf.com and have a problem, Note: This form should only be used to report bugs in the
or if you would like to request a new feature please contact Extended CE release of Overleaf.
the support team at support@overleaf.com
This form should only be used to report bugs in the
Community Edition release of Overleaf.
--> -->
-4
View File
@@ -5,7 +5,3 @@
## Related issues / Pull Requests ## Related issues / Pull Requests
<!-- Fixes #xyz, Contributes to #xyz, Related to #xyz--> <!-- Fixes #xyz, Contributes to #xyz, Related to #xyz-->
## Contributor Agreement
- [ ] I confirm I have signed the [Contributor License Agreement](https://github.com/overleaf/overleaf/blob/main/CONTRIBUTING.md#contributor-license-agreement)
@@ -0,0 +1,23 @@
diff --git a/lib/saml.js b/lib/saml.js
index 64180fdd7fe6302bf8da7d1ca65c01bb1ea5e2c1..7015c3b73603f624c15195a0a38c2fbf750a4c60 100644
--- a/lib/saml.js
+++ b/lib/saml.js
@@ -327,7 +327,8 @@ class SAML {
const requestOrResponse = request || response;
(0, utility_1.assertRequired)(requestOrResponse, "either request or response is required");
let buffer;
- if (this.options.skipRequestCompression) {
+ // logout requestOrResponse must be compressed anyway
+ if (this.options.skipRequestCompression && operation !== "logout") {
buffer = Buffer.from(requestOrResponse, "utf8");
}
else {
@@ -510,7 +511,7 @@ class SAML {
try {
xml = Buffer.from(container.SAMLResponse, "base64").toString("utf8");
doc = await (0, xml_1.parseDomFromString)(xml);
- const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response']/@InResponseTo");
+ const inResponseToNodes = xml_1.xpath.selectAttributes(doc, "/*[local-name()='Response' or local-name()='LogoutResponse']/@InResponseTo");
if (inResponseToNodes) {
inResponseTo = inResponseToNodes.length ? inResponseToNodes[0].nodeValue : null;
await this.validateInResponseTo(inResponseTo);
@@ -0,0 +1,73 @@
diff --git a/lib/ldapauth.js b/lib/ldapauth.js
index 88398ed31afee34c8958ad5e7a53653e1b5f6f82..0334177af8a65c7c5f9de7570924ab0f359e3f38 100644
--- a/lib/ldapauth.js
+++ b/lib/ldapauth.js
@@ -87,6 +87,7 @@ function LdapAuth(opts) {
this.opts.bindProperty || (this.opts.bindProperty = 'dn');
this.opts.groupSearchScope || (this.opts.groupSearchScope = 'sub');
this.opts.groupDnProperty || (this.opts.groupDnProperty = 'dn');
+ this.opts.tlsStarted = false;
EventEmitter.call(this);
@@ -126,21 +127,7 @@ function LdapAuth(opts) {
this._userClient.on('error', this._handleError.bind(this));
var self = this;
- if (this.opts.starttls) {
- // When starttls is enabled, this callback supplants the 'connect' callback
- this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
- if (err) {
- self._handleError(err);
- } else {
- self._onConnectAdmin();
- }
- });
- this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
- if (err) {
- self._handleError(err);
- }
- });
- } else if (opts.reconnect) {
+ if (opts.reconnect && !this.opts.starttls) {
this.once('_installReconnectListener', function () {
self.log && self.log.trace('install reconnect listener');
self._adminClient.on('connect', function () {
@@ -401,6 +388,37 @@ LdapAuth.prototype._findGroups = function (user, callback) {
*/
LdapAuth.prototype.authenticate = function (username, password, callback) {
var self = this;
+ if (this.opts.starttls && !this.opts.tlsStarted) {
+ // When starttls is enabled, this callback supplants the 'connect' callback
+ this._adminClient.starttls(this.opts.tlsOptions, this._adminClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ } else {
+ self._onConnectAdmin(function(){self._handleAuthenticate(username, password, callback);});
+ }
+ });
+ this._userClient.starttls(this.opts.tlsOptions, this._userClient.controls, function (err) {
+ if (err) {
+ self._handleError(err);
+ }
+ });
+ } else {
+ self._handleAuthenticate(username, password, callback);
+ }
+};
+
+/**
+ * Authenticate given credentials against LDAP server
+ *
+ * @private
+ * @param {string} username - The username to authenticate
+ * @param {string} password - The password to verify
+ * @param {resultCallback} callback - Result handling callback
+ * @returns {undefined}
+ */
+LdapAuth.prototype._handleAuthenticate = function (username, password, callback) {
+ this.opts.tlsStarted = true;
+ var self = this;
if (typeof password === 'undefined' || password === null || password === '') {
return callback(new Error('no password given'));
+6 -15
View File
@@ -1,38 +1,29 @@
# Contributing to Overleaf # Contributing to Overleaf CE+
Thank you for reading this! If you'd like to report a bug or join in the development Thank you for reading this! If you'd like to report a bug or join in the development
of Overleaf, then here are some notes on how to do that. of Overleaf CE+, then here are some notes on how to do that.
## Reporting bugs and opening issues ## Reporting bugs and opening issues
If you'd like to report a bug or open an issue, please **[check if there is an existing issue](https://github.com/overleaf/overleaf/issues).** If you'd like to report a bug or open an issue, please **[check if there is an existing issue](https://github.com/yu-i-i/overleaf-cep).**
If there is then please add any more information that you have, or give it a 👍. If there is then please add any more information that you have, or give it a 👍.
When submitting an issue please describe the issue as clearly as possible, including how to When submitting an issue please describe the issue as clearly as possible, including how to
reproduce the bug, which situations it appears in, what you expected to happen, and what actually happens. reproduce the bug, which situations it appears in, what you expected to happen, and what actually happens.
If you can include a screenshot for front end issues that is very helpful. If you can include a screenshot for front end issues that is very helpful.
**Note**: If you are using [www.overleaf.com](www.overleaf.com) and have a problem, or if you would like to request a new feature, please contact the Support team at support@overleaf.com. Raise an issue here only to report bugs in the Community Edition release of Overleaf.
## Pull Requests ## Pull Requests
See [our wiki](https://github.com/overleaf/overleaf/wiki) See [Overleaf wiki](https://github.com/overleaf/overleaf/wiki)
for how to manage the Overleaf development environment and for our developer guidelines. for how to manage the Overleaf development environment and for our developer guidelines.
We love pull requests, so be bold with them! Don't be afraid of going ahead We love pull requests, so be bold with them! Don't be afraid of going ahead
and changing something, or adding a new feature. We're very happy to work with you and changing something, or adding a new feature. We're very happy to work with you
to get your changes merged into Overleaf. to get your changes merged into Overleaf.
If you're looking for something to work on, have a look at the [open issues](https://github.com/overleaf/overleaf/issues). If you're looking for something to work on, have a look at the [open issues](https://github.com/yu-i-i/overleaf-cep/issues).
## Security ## Security
Please see [our security policy](https://github.com/overleaf/overleaf/security/policy) if you would like to report a potential security vulnerability. Please see [Overleaf security policy](https://github.com/overleaf/overleaf/security/policy) if you would like to report a potential security vulnerability.
## Contributor License Agreement
Before we can accept any contributions of code, we need you to agree to our
[Contributor License Agreement](https://docs.google.com/forms/d/e/1FAIpQLSef79XH3mb7yIiMzZw-yALEegS-wyFetvjTiNBfZvf_IHD2KA/viewform?usp=sf_link).
This is to ensure that you own the copyright of your contribution, and that you
agree to give us a license to use it in both the open source version, and the version
of Overleaf running at www.overleaf.com, which may have additional changes.
+53
View File
@@ -0,0 +1,53 @@
# CREDITS
This project includes ideas and adapted code from several open-source projects.\
In most cases the original code has been modified, optimized, or extended.
## Symbol palette
The symbol palette feature is based on the
[original Overleaf implementation](https://github.com/overleaf/web/tree/master/frontend/js/features/symbol-palette)
The original code was slightly improved, particularly in the parts related to keyboard input.
## LDAP authentication
LDAP authentication was inspired by [this project](https://github.com/smhaller/ldap-overleaf-sl).
The project provided the idea and part of the implementation for adding LDAP users to the user's contacts.
## Real-time track changes and comments
The Track Changes and Comments feature exists largely in the original Overleaf codebase.\
The missing parts were implemented based on [this code](https://github.com/ertuil/overleaf).
The referenced code was fixed, optimized, and extended.
## Sandboxed compiles
The Sandboxed Compiles feature was until recently largely present in the original Overleaf codebase.\
The missing parts were implemented in this project.
## Import file from external URL
The "From External URL" feature exists in the original Overleaf code.\
The missing proxy component was implemented in this project.
## Git integration
The Git integration feature includes parts of the frontend code
from [here](https://github.com/ayaka-notes/overleaf-pro/tree/feat-git-bridge)
## GitHub synchronization
The GitHub synchronization feature includes parts of the frontend code and the OAuth2 backend implementation
from [here](https://github.com/ayaka-notes/overleaf-pro/commit/06a30fe9a0ed75e5ab40b50a8a4e94f43161cf71).
## Sign Up page
The Sign Up page is based on [this code](https://github.com/ayaka-notes/overleaf-pro/tree/feat-public-registeration).
# Acknowledgments
Thanks to the users of the project for valuable feedback, suggestions,
and help in identifying and fixing bugs.
+44 -31
View File
@@ -6,52 +6,65 @@
<h4 align="center">An open-source online real-time collaborative LaTeX editor.</h4> <h4 align="center">An open-source online real-time collaborative LaTeX editor.</h4>
<p align="center"> <p align="center">
<a href="https://github.com/overleaf/overleaf/wiki">Wiki</a> • <a href="https://github.com/yu-i-i/overleaf-cep/wiki">Wiki</a> •
<a href="https://www.overleaf.com/for/enterprises">Server Pro</a> • <a href="https://www.overleaf.com/for/enterprises">Server Pro</a> •
<a href="#contributing">Contributing</a> •
<a href="https://mailchi.mp/overleaf.com/community-edition-and-server-pro">Mailing List</a> •
<a href="#authors">Authors</a> • <a href="#authors">Authors</a> •
<a href="#license">License</a> <a href="#license">License</a>
</p> </p>
<img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Community Edition"> <img src="doc/screenshot.png" alt="A screenshot of a project being edited in Overleaf Extended Community Edition">
<p align="center"> <p align="center">
Figure 1: A screenshot of a project being edited in Overleaf Community Edition. Figure 1: A screenshot of a project being edited in Overleaf Extended Community Edition.
</p> </p>
## Community Edition ## Community Edition
[Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. We run a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf. [Overleaf](https://www.overleaf.com) is an open-source online real-time collaborative LaTeX editor. Overleaf runs a hosted version at [www.overleaf.com](https://www.overleaf.com), but you can also run your own local version, and contribute to the development of Overleaf.
## Extended Community Edition (CE+)
The present "extended" version of Overleaf CE includes:
- Sandboxed compiles with TeX Live image selection
- Sign Up page
- LDAP authentication
- SAML authentication
- OpenID Connect authentication
- Real-time track changes and comments
- Symbol palette
- Template gallery
- Import file from external URL
- Git integration
- GitHub synchronization
- Zotero integration
- Reference Search and Pick Tool
- Document Import (`.docx`, `.md`) and Export (`.docx`, `.md`, `.html`)
- Advanced administrator tools for managing user accounts and projects
- Logo tools
> [!CAUTION] > [!CAUTION]
> Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles. > Overleaf Community Edition is intended for use in environments where **all** users are trusted. Community Edition is **not** appropriate for scenarios where isolation of users is required due to Sandbox Compiles not being available. When not using Sandboxed Compiles, users have full read and write access to the `sharelatex` container resources (filesystem, network, environment variables) when running LaTeX compiles.
Therefore, in any environment where not all users can be fully trusted, it is strongly recommended to enable the Sandboxed Compiles feature available in the Extended Community Edition.
For more information on Sandbox Compiles check out our [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles). For more information on Sandbox Compiles check out Overleaf [documentation](https://docs.overleaf.com/on-premises/configuration/overleaf-toolkit/server-pro-only-configuration/sandboxed-compiles).
## Enterprise ## Enterprise
If you want help installing and maintaining Overleaf in your lab or workplace, we offer an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises). It also includes more features for security (SSO with LDAP or SAML), administration and collaboration (e.g. tracked changes). [Find out more!](https://www.overleaf.com/for/enterprises) If you want help installing and maintaining Overleaf in your lab or workplace, Overleaf offers an officially supported version called [Overleaf Server Pro](https://www.overleaf.com/for/enterprises).
## Keeping up to date
Sign up to the [mailing list](https://mailchi.mp/overleaf.com/community-edition-and-server-pro) to get updates on Overleaf releases and development.
## Installation ## Installation
We have detailed installation instructions in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/). Detailed installation instructions can be found in the [Overleaf Toolkit](https://github.com/overleaf/toolkit/).
Configuration details and release history for the Extended Community Edition can be found on the [Extended CE Wiki Page](https://github.com/yu-i-i/overleaf-cep/wiki).
## Upgrading
If you are upgrading from a previous version of Overleaf, please see the [Release Notes section on the Wiki](https://github.com/overleaf/overleaf/wiki#release-notes) for all of the versions between your current version and the version you are upgrading to.
## Overleaf Docker Image ## Overleaf Docker Image
This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the This repo contains two dockerfiles, [`Dockerfile-base`](server-ce/Dockerfile-base), which builds the
`sharelatex/sharelatex-base` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the `sharelatex/sharelatex-base:ext-ce` image, and [`Dockerfile`](server-ce/Dockerfile) which builds the
`sharelatex/sharelatex` (or "community") image. `sharelatex/sharelatex:ext-ce` image.
The Base image generally contains the basic dependencies like `wget`, plus `texlive`. The Base image generally contains the basic dependencies like `wget`, plus `texlive`.
We split this out because it's a pretty heavy set of This is split out because it's a pretty heavy set of
dependencies, and it's nice to not have to rebuild all of that every time. dependencies, and it's nice to not have to rebuild all of that every time.
The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code The `sharelatex/sharelatex` image extends the base image and adds the actual Overleaf code
@@ -59,22 +72,22 @@ and services.
Use `make build-base` and `make build-community` from `server-ce/` to build these images. Use `make build-base` and `make build-community` from `server-ce/` to build these images.
We use the [Phusion base-image](https://github.com/phusion/baseimage-docker) The [Phusion base-image](https://github.com/phusion/baseimage-docker)
(which is extended by our `base` image) to provide us with a VM-like container (which is extended by the `base` image) provides a VM-like container
in which to run the Overleaf services. Baseimage uses the `runit` service in which to run the Overleaf services. Baseimage uses the `runit` service
manager to manage services, and we add our init-scripts from the `server-ce/runit` manager to manage services, and init scripts from the `server-ce/runit`
folder. folder are added.
## Contributing
Please see the [CONTRIBUTING](CONTRIBUTING.md) file for information on contributing to the development of Overleaf.
## Authors ## Authors
[The Overleaf Team](https://www.overleaf.com/about) [The Overleaf Team](https://www.overleaf.com/about)\
[yu-i-i](https://github.com/yu-i-i), [davrot](https://github.com/davrot) — CE extensions; references to adapted code are listed in [`CREDITS`](CREDITS.md)
## License ## License
The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the [`LICENSE`](LICENSE) file. The code in this repository is released under the GNU AFFERO GENERAL PUBLIC LICENSE, version 3. A copy can be found in the [`LICENSE`](LICENSE) file.
Copyright (c) Overleaf, 2014-2025. Copyright (c) Overleaf, 2014-2026.\
Copyright (c) @yu-i-i, 2024-2026, for CE extensions.
Portions of the code are derived from other open-source projects; see [`CREDITS`](CREDITS.md).
+1
View File
@@ -66,6 +66,7 @@ each service:
| `real-time` | 9237 | | `real-time` | 9237 |
| `history-v1` | 9239 | | `history-v1` | 9239 |
| `project-history` | 9240 | | `project-history` | 9240 |
| `linked-url-proxy` | 9241 |
To attach to a service using Chrome's _remote debugging_, go to To attach to a service using Chrome's _remote debugging_, go to
<chrome://inspect/> and make sure _Discover network targets_ is checked. Next <chrome://inspect/> and make sure _Discover network targets_ is checked. Next
+1
View File
@@ -7,6 +7,7 @@ FILESTORE_HOST=filestore
GRACEFUL_SHUTDOWN_DELAY_SECONDS=0 GRACEFUL_SHUTDOWN_DELAY_SECONDS=0
HISTORY_V1_HOST=history-v1 HISTORY_V1_HOST=history-v1
HISTORY_REDIS_HOST=redis HISTORY_REDIS_HOST=redis
LINKED_URL_PROXY_HOST=linked-url-proxy
LISTEN_ADDRESS=0.0.0.0 LISTEN_ADDRESS=0.0.0.0
MONGO_HOST=mongo MONGO_HOST=mongo
MONGO_URL=mongodb://mongo/sharelatex?directConnection=true MONGO_URL=mongodb://mongo/sharelatex?directConnection=true
+11
View File
@@ -68,6 +68,17 @@ services:
- ../services/history-v1/knexfile.js:/overleaf/services/history-v1/knexfile.js - ../services/history-v1/knexfile.js:/overleaf/services/history-v1/knexfile.js
- ../services/history-v1/migrations:/overleaf/services/history-v1/migrations - ../services/history-v1/migrations:/overleaf/services/history-v1/migrations
linked-url-proxy:
command: ["node", "--watch", "app.mjs"]
environment:
- NODE_OPTIONS=--inspect=0.0.0.0:9229
ports:
- "127.0.0.1:9241:9229"
volumes:
- ../services/linked-url-proxy/app:/overleaf/services/linked-url-proxy/app
- ../services/linked-url-proxy/config:/overleaf/services/linked-url-proxy/config
- ../services/linked-url-proxy/app.mjs:/overleaf/services/linked-url-proxy/app.mjs
notifications: notifications:
command: ["node", "--watch", "app.ts"] command: ["node", "--watch", "app.ts"]
environment: environment:
+24 -1
View File
@@ -88,6 +88,13 @@ services:
volumes: volumes:
- history-v1-buckets:/buckets - history-v1-buckets:/buckets
linked-url-proxy:
build:
context: ..
dockerfile: services/linked-url-proxy/Dockerfile
env_file:
- dev.env
mongo: mongo:
image: mongo:8 image: mongo:8
command: --replSet overleaf command: --replSet overleaf
@@ -141,7 +148,7 @@ services:
- dev.env - dev.env
environment: environment:
- APP_NAME=Overleaf Community Edition - APP_NAME=Overleaf Community Edition
- ENABLED_LINKED_FILE_TYPES=project_file,project_output_file - ENABLED_LINKED_FILE_TYPES=project_file,project_output_file,zotero
- EMAIL_CONFIRMATION_DISABLED=true - EMAIL_CONFIRMATION_DISABLED=true
- NODE_ENV=development - NODE_ENV=development
- OVERLEAF_ALLOW_PUBLIC_ACCESS=true - OVERLEAF_ALLOW_PUBLIC_ACCESS=true
@@ -158,6 +165,7 @@ services:
- document-updater - document-updater
- filestore - filestore
- history-v1 - history-v1
- linked-url-proxy
- notifications - notifications
- project-history - project-history
- real-time - real-time
@@ -173,3 +181,18 @@ services:
- "127.0.0.1:80:3808" - "127.0.0.1:80:3808"
volumes: volumes:
- ./webpack.config.dev-env.js:/overleaf/services/web/webpack.config.dev-env.js - ./webpack.config.dev-env.js:/overleaf/services/web/webpack.config.dev-env.js
git-bridge:
image: quay.io/sharelatex/git-bridge:latest
expose:
- "8000"
ports:
- "8000:8000"
environment:
GIT_BRIDGE_API_BASE_URL: "http://web:3000/api/v0"
GIT_BRIDGE_OAUTH2_SERVER: "http://web:3000"
GIT_BRIDGE_POSTBACK_BASE_URL: "http://git-bridge:8000"
GIT_BRIDGE_ROOT_DIR: "/data/git-bridge"
LOG_LEVEL: "DEBUG"
volumes:
- ./data/git-bridge:/data/git-bridge
+5
View File
@@ -14,6 +14,11 @@ module.exports = merge(base, {
target: 'http://real-time:3026', target: 'http://real-time:3026',
ws: true, ws: true,
}, },
{
context: '/git/**',
target: 'http://git-bridge:8000',
pathRewrite: { '^/git': '' }
},
{ {
context: ['!**/*.js', '!**/*.css', '!**/*.json'], context: ['!**/*.js', '!**/*.css', '!**/*.json'],
target: 'http://web:3000', target: 'http://web:3000',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 KiB

After

Width:  |  Height:  |  Size: 525 KiB

+1 -1
View File
@@ -33,7 +33,7 @@ services:
# IMPORTANT: Required for invite token generation # IMPORTANT: Required for invite token generation
# OVERLEAF_INVITE_TOKEN_SECRET: generate with `openssl rand -base64 32` # OVERLEAF_INVITE_TOKEN_SECRET: generate with `openssl rand -base64 32`
ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file" ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file,zotero"
# Enables Thumbnail generation using ImageMagick # Enables Thumbnail generation using ImageMagick
ENABLE_CONVERSIONS: "true" ENABLE_CONVERSIONS: "true"
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/access-token-encryptor monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/access-token-encryptor
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/access-token-encryptor
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/access-token-encryptor
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
@@ -5,6 +5,5 @@ access-token-encryptor
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/access-token-encryptor
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/fetch-utils monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/fetch-utils
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/fetch-utils
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/fetch-utils
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ fetch-utils
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/fetch-utils
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/logger monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/logger
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/logger
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/logger
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ logger
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/logger
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/metrics monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/metrics
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/metrics
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/metrics
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ metrics
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/metrics
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/mongo-utils monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/mongo-utils
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/mongo-utils
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/mongo-utils
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,7 +5,6 @@ mongo-utils
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/mongo-utils
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
--tsconfig-no-implicit-any=True --tsconfig-no-implicit-any=True
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/o-error monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/o-error
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/o-error
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/o-error
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ o-error
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/o-error
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/object-persistor monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/object-persistor
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/object-persistor
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/object-persistor
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
@@ -5,7 +5,6 @@ object-persistor
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/object-persistor
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
--tsconfig-no-implicit-any=True --tsconfig-no-implicit-any=True
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/overleaf-editor-core monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/overleaf-editor-core
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/overleaf-editor-core
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/overleaf-editor-core
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
@@ -5,6 +5,5 @@ overleaf-editor-core
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=overleaf-editor-core
--pipeline-owner=44 --pipeline-owner=44
--public-repo=False --public-repo=False
@@ -56,8 +56,7 @@ class TrackedChange {
return false return false
} }
return ( return (
this.tracking.type === other.tracking.type && this.tracking.canMergeWith(other.tracking) &&
this.tracking.userId === other.tracking.userId &&
this.range.touches(other.range) && this.range.touches(other.range) &&
this.range.canMerge(other.range) this.range.canMerge(other.range)
) )
@@ -75,13 +74,7 @@ class TrackedChange {
} }
return new TrackedChange( return new TrackedChange(
this.range.merge(other.range), this.range.merge(other.range),
new TrackingProps( this.tracking.mergeWith(other.tracking)
this.tracking.type,
this.tracking.userId,
this.tracking.ts.getTime() > other.tracking.ts.getTime()
? this.tracking.ts
: other.tracking.ts
)
) )
} }
@@ -405,6 +405,49 @@ describe('TextOperation', function () {
}) })
) )
it(
'compose associativity (randomised)',
random.test(numTrials, () => {
const str = random.string(20)
const comments = random.comments(6)
const a = randomOperation(str, comments.ids)
const afterA = new StringFileData(str, comments.comments)
a.apply(afterA)
const b = randomOperation(afterA.getContent(), comments.ids)
const afterB = new StringFileData(
afterA.getContent(),
comments.comments
)
b.apply(afterB)
const c = randomOperation(afterB.getContent(), comments.ids)
const ab = a.compose(b)
const ab_c = ab.compose(c)
const bc = b.compose(c)
const a_bc = a.compose(bc)
const ab_c_file = new StringFileData(str, comments.comments)
ab_c.apply(ab_c_file)
const a_bc_file = new StringFileData(str, comments.comments)
a_bc.apply(a_bc_file)
const fuzzingError = fuzzingErrorMessage({
str,
comments,
a: a.toJSON(),
b: b.toJSON(),
c: c.toJSON(),
})
expect(ab_c_file.toRaw()).to.deep.equal(a_bc_file.toRaw(), fuzzingError)
})
)
it('composes two operations with comments', function () { it('composes two operations with comments', function () {
expect( expect(
compose( compose(
@@ -625,11 +668,54 @@ describe('TextOperation', function () {
a: a.toJSON(), a: a.toJSON(),
b: b.toJSON(), b: b.toJSON(),
}) })
expect(abPrime.equals(baPrime)).to.be.equal(true, fuzzingError) // The composition of ab' and ba' is not guaranteed to be equal, but
// should converge to the same file contents + ranges.
expect(abFile.toRaw()).to.deep.equal(baFile.toRaw(), fuzzingError) expect(abFile.toRaw()).to.deep.equal(baFile.toRaw(), fuzzingError)
}) })
) )
it('chooses lower tracked change timestamp', function () {
const ts1 = '2024-01-01T01:00:00.000Z'
const ts2 = '2024-01-01T02:00:00.000Z'
const str = 'abcde'
const comments = []
const a = new TextOperation()
.retain(2, {
tracking: TrackingProps.fromRaw({
ts: ts1,
type: 'insert',
userId: 'user1',
}),
})
.retain(1)
.retain(2, {
tracking: TrackingProps.fromRaw({
ts: ts2,
type: 'insert',
userId: 'user1',
}),
})
const b = new TextOperation().retain(1).remove(3).retain(1)
const [aPrime, bPrime] = TextOperation.transform(a, b)
const aComposeBPrime = a.compose(bPrime)
const bComposeAPrime = b.compose(aPrime)
const aBPFile = new StringFileData(str, comments)
aComposeBPrime.apply(aBPFile)
const bAPFile = new StringFileData(str, comments)
bComposeAPrime.apply(bAPFile)
expect(aBPFile.toRaw()).to.deep.equal(bAPFile.toRaw())
expect(aBPFile.trackedChanges.length).to.equal(1)
expect(
aBPFile.trackedChanges.asSorted()[0].tracking.ts.toISOString()
).to.equal(ts1)
})
it('adds a tracked change from operation 1', function () { it('adds a tracked change from operation 1', function () {
expect( expect(
transform( transform(
@@ -8,7 +8,7 @@ const { expect } = require('chai')
describe('TrackedChangeList', function () { describe('TrackedChangeList', function () {
describe('applyInsert', function () { describe('applyInsert', function () {
describe('with same author', function () { describe('with same author', function () {
it('should merge consecutive tracked changes and use the latest timestamp', function () { it('should merge consecutive tracked changes and use the earliest timestamp', function () {
const trackedChanges = TrackedChangeList.fromRaw([ const trackedChanges = TrackedChangeList.fromRaw([
{ {
range: { pos: 0, length: 3 }, range: { pos: 0, length: 3 },
@@ -33,7 +33,7 @@ describe('TrackedChangeList', function () {
tracking: { tracking: {
type: 'insert', type: 'insert',
userId: 'user1', userId: 'user1',
ts: '2024-01-01T00:00:00.000Z', ts: '2023-01-01T00:00:00.000Z',
}, },
}, },
]) ])
@@ -699,7 +699,7 @@ describe('TrackedChangeList', function () {
tracking: { tracking: {
type: 'insert', type: 'insert',
userId: 'user1', userId: 'user1',
ts: '2024-01-01T00:00:00.000Z', ts: '2023-01-01T00:00:00.000Z',
}, },
}, },
]) ])
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/promise-utils monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/promise-utils
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/promise-utils
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/promise-utils
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ promise-utils
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/promise-utils
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/ranges-tracker monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/ranges-tracker
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/ranges-tracker
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/ranges-tracker
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ ranges-tracker
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/ranges-tracker
--pipeline-owner=44 --pipeline-owner=44
--public-repo=False --public-repo=False
+16 -1
View File
@@ -420,7 +420,7 @@ class RangesTracker {
offset = opStart - changeStart offset = opStart - changeStart
change.op.i = change.op.i =
change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset) change.op.i.slice(0, offset) + op.i + change.op.i.slice(offset)
change.metadata.ts = metadata.ts change.metadata.ts = pickTimestamp(change.metadata, metadata)
alreadyMerged = true alreadyMerged = true
movedChanges.push(change) movedChanges.push(change)
} else if (opStart <= changeStart) { } else if (opStart <= changeStart) {
@@ -728,6 +728,10 @@ class RangesTracker {
) { ) {
removeChanges.push(change) removeChanges.push(change)
previousChange.op.i += change.op.i previousChange.op.i += change.op.i
previousChange.metadata.ts = pickTimestamp(
previousChange.metadata,
change.metadata
)
movedChanges.push(previousChange) movedChanges.push(previousChange)
} }
} else if ( } else if (
@@ -790,4 +794,15 @@ class RangesTracker {
} }
} }
function pickTimestamp(oldMetadata, newMetadata) {
// Make sure null values don't get treated as 1970-01-01 dates in the
// comparison below.
if (oldMetadata.ts == null) return newMetadata.ts
if (newMetadata.ts == null) return oldMetadata.ts
return new Date(oldMetadata.ts) < new Date(newMetadata.ts)
? oldMetadata.ts
: newMetadata.ts
}
module.exports = RangesTracker module.exports = RangesTracker
@@ -222,4 +222,114 @@ describe('RangesTracker', function () {
) )
}) })
}) })
describe('merging tracked inserts from the same user', function () {
beforeEach(function () {
this.comments = []
})
it('keeps the earlier timestamp when the existing insert is older', function () {
this.rangesTracker = new RangesTracker(
[
{
id: 'id1',
op: { p: 10, i: 'foo' },
metadata: { user_id: 'user-1', ts: '2024-01-01T00:00:00.000Z' },
},
],
this.comments
)
this.rangesTracker.track_changes = true
this.rangesTracker.applyOp(
{ p: 13, i: 'bar' },
{ user_id: 'user-1', ts: new Date('2024-06-01T00:00:00.000Z') }
)
expect(this.rangesTracker.changes).to.have.length(1)
const [change] = this.rangesTracker.changes
expect(change.op).to.deep.equal({ p: 10, i: 'foobar' })
expect(change.metadata.user_id).to.equal('user-1')
expect(new Date(change.metadata.ts).toISOString()).to.equal(
'2024-01-01T00:00:00.000Z'
)
})
it('keeps the earlier timestamp when the incoming insert is older', function () {
this.rangesTracker = new RangesTracker(
[
{
id: 'id1',
op: { p: 10, i: 'foo' },
metadata: { user_id: 'user-1', ts: '2024-06-01T00:00:00.000Z' },
},
],
this.comments
)
this.rangesTracker.track_changes = true
this.rangesTracker.applyOp(
{ p: 13, i: 'bar' },
{ user_id: 'user-1', ts: new Date('2024-01-01T00:00:00.000Z') }
)
expect(this.rangesTracker.changes).to.have.length(1)
const [change] = this.rangesTracker.changes
expect(change.op).to.deep.equal({ p: 10, i: 'foobar' })
expect(change.metadata.user_id).to.equal('user-1')
expect(new Date(change.metadata.ts).toISOString()).to.equal(
'2024-01-01T00:00:00.000Z'
)
})
it('uses the defined timestamp when the existing insert has a null timestamp', function () {
this.rangesTracker = new RangesTracker(
[
{
id: 'id1',
op: { p: 10, i: 'foo' },
metadata: { user_id: 'user-1', ts: null },
},
],
this.comments
)
this.rangesTracker.track_changes = true
this.rangesTracker.applyOp(
{ p: 13, i: 'bar' },
{ user_id: 'user-1', ts: new Date('2024-01-01T00:00:00.000Z') }
)
expect(this.rangesTracker.changes).to.have.length(1)
const [change] = this.rangesTracker.changes
expect(new Date(change.metadata.ts).toISOString()).to.equal(
'2024-01-01T00:00:00.000Z'
)
})
it('keeps the earlier timestamp when re-merging inserts after a deletion', function () {
this.rangesTracker = new RangesTracker(
[
{
id: 'id1',
op: { p: 10, i: 'aaa' },
metadata: { user_id: 'user-1', ts: '2024-03-01T00:00:00.000Z' },
},
{
id: 'id2',
op: { p: 13, i: 'bb' },
metadata: { user_id: 'user-2', ts: '2024-02-01T00:00:00.000Z' },
},
{
id: 'id3',
op: { p: 15, i: 'ccc' },
metadata: { user_id: 'user-1', ts: '2024-01-01T00:00:00.000Z' },
},
],
this.comments
)
this.rangesTracker.applyOp({ p: 13, d: 'bb' })
expect(this.rangesTracker.changes).to.have.length(1)
const [change] = this.rangesTracker.changes
expect(change.op).to.deep.equal({ p: 10, i: 'aaaccc' })
expect(change.metadata.user_id).to.equal('user-1')
expect(new Date(change.metadata.ts).toISOString()).to.equal(
'2024-01-01T00:00:00.000Z'
)
})
})
}) })
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/redis-wrapper monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/redis-wrapper
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/redis-wrapper
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/redis-wrapper
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ redis-wrapper
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/redis-wrapper
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/settings monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/settings
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/settings
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/settings
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ settings
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/settings
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/stream-utils monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/stream-utils
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/stream-utils
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/stream-utils
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
-1
View File
@@ -5,6 +5,5 @@ stream-utils
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/stream-utils
--pipeline-owner=32 --pipeline-owner=32
--public-repo=False --public-repo=False
+59
View File
@@ -0,0 +1,59 @@
# Run the test commands in the scope of the monorepo.
# Eslint and prettier (plus some configs) are on the root.
RUN_LINTING = ../../bin/run -w /overleaf/libraries/validation-tools monorepo yarn run --silent
RUN_LINTING_MONOREPO = ../../bin/run monorepo yarn run --silent
WITH_CACHE_FOLDER = \
lint \
lint_ci \
lint_fix \
format \
format_ci \
format_fix \
typecheck \
typecheck_ci \
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/prettier/
../../node_modules/.cache/prettier/:
mkdir -p $@
$(WITH_CACHE_FOLDER): ../../node_modules/.cache/eslint/
../../node_modules/.cache/eslint/:
mkdir -p $@
format:
$(RUN_LINTING_MONOREPO) format libraries/validation-tools
format_ci:
$(RUN_LINTING_MONOREPO) format libraries/validation-tools
format_fix:
$(RUN_LINTING_MONOREPO) format:fix libraries/validation-tools
lint:
$(RUN_LINTING) lint
lint_ci:
-$(RUN_LINTING) lint --format json --output-file reports/eslint.json
sed -i 's_"filePath":"/overleaf_"filePath":"$(MONOREPO)_g' reports/eslint.json
lint_fix:
$(RUN_LINTING) lint:fix
typecheck:
$(RUN_LINTING) types:check
typecheck_ci:
$(RUN_LINTING) types:check
test: test_unit
test_unit:
ifneq (,$(wildcard test/unit))
$(RUN_LINTING) test:unit
endif
test: test_acceptance
test_acceptance:
ifneq (,$(wildcard test/acceptance))
$(RUN_LINTING) test:acceptance
endif
@@ -5,7 +5,6 @@ validation-tools
--esmock-loader=False --esmock-loader=False
--is-library=True --is-library=True
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/validation-tools
--public-repo=False --public-repo=False
--test-acceptance-vitest=True --test-acceptance-vitest=True
--test-unit-vitest=True --test-unit-vitest=True
+11 -5
View File
@@ -41,12 +41,13 @@
"sandboxed-module": "patch:sandboxed-module@npm%3A2.0.4#~/.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch", "sandboxed-module": "patch:sandboxed-module@npm%3A2.0.4#~/.yarn/patches/sandboxed-module-npm-2.0.4-f8b45aacc9.patch",
"request/tough-cookie": "5.1.2", "request/tough-cookie": "5.1.2",
"request/form-data": "2.5.5", "request/form-data": "2.5.5",
"request/qs": "6.14.1", "request/qs": "6.15.2",
"@cypress/request/qs": "6.14.1", "@cypress/request/qs": "6.15.2",
"qs": "6.15.2",
"@opentelemetry/api": "1.9.0", "@opentelemetry/api": "1.9.0",
"mocha/serialize-javascript": "7.0.5", "mocha/serialize-javascript": "7.0.5",
"pprof/protobufjs": "7.5.5", "protobufjs": "7.6.3",
"@google-cloud/profiler/protobufjs": "7.5.5", "@protobufjs/utf8": "1.1.1",
"mocha-multi-reporters": "patch:mocha-multi-reporters@npm%3A1.5.1#~/.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch", "mocha-multi-reporters": "patch:mocha-multi-reporters@npm%3A1.5.1#~/.yarn/patches/mocha-multi-reporters-npm-1.5.1-0a1088aed5.patch",
"pdfjs-dist": "patch:pdfjs-dist@npm%3A5.1.91#~/.yarn/patches/pdfjs-dist-npm-5.1.91.patch", "pdfjs-dist": "patch:pdfjs-dist@npm%3A5.1.91#~/.yarn/patches/pdfjs-dist-npm-5.1.91.patch",
"referer-parser": "patch:referer-parser@npm%3A0.0.3#~/.yarn/patches/referer-parser-npm-0.0.3.patch", "referer-parser": "patch:referer-parser@npm%3A0.0.3#~/.yarn/patches/referer-parser-npm-0.0.3.patch",
@@ -101,11 +102,12 @@
"cluster-key-slot": "1.1.0", "cluster-key-slot": "1.1.0",
"@octokit/request": "9.2.2", "@octokit/request": "9.2.2",
"randomstring": "1.2.2", "randomstring": "1.2.2",
"vite": "7.3.1", "vite": "7.3.5",
"isomorphic-git": "1.33.1", "isomorphic-git": "1.33.1",
"http-status": "1.5.0", "http-status": "1.5.0",
"knex": "2.4.0", "knex": "2.4.0",
"utf-8-validate": "5.0.8", "utf-8-validate": "5.0.8",
"ws@npm:^8": "8.21.0",
"samlp": "7.0.2", "samlp": "7.0.2",
"compression": "1.7.4", "compression": "1.7.4",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
@@ -178,6 +180,8 @@
"fast-xml-builder": "1.1.7", "fast-xml-builder": "1.1.7",
"fast-xml-parser": "5.7.3", "fast-xml-parser": "5.7.3",
"ip-address": "10.1.1", "ip-address": "10.1.1",
"ldapauth-fork@npm:^5.0.1": "patch:ldapauth-fork@npm%3A5.0.5#~/.yarn/patches/ldapauth-fork-npm-5.0.5-c8b86f5231.patch",
"@node-saml/node-saml@npm:^5.1.0": "patch:@node-saml/node-saml@npm%3A5.1.0#~/.yarn/patches/@node-saml-node-saml-npm-5.1.0-f4eb6913cc.patch",
"systeminformation": "5.31.6" "systeminformation": "5.31.6"
}, },
"scripts": { "scripts": {
@@ -196,6 +200,7 @@
}, },
"workspaces": [ "workspaces": [
"jobs/mirror-documentation", "jobs/mirror-documentation",
"jobs/wiki-algolia",
"libraries/*", "libraries/*",
"server-ce/test", "server-ce/test",
"services/analytics", "services/analytics",
@@ -212,6 +217,7 @@
"services/history-v1", "services/history-v1",
"services/idp", "services/idp",
"services/latexqc", "services/latexqc",
"services/linked-url-proxy",
"services/notifications", "services/notifications",
"services/project-history", "services/project-history",
"services/real-time", "services/real-time",
+4 -2
View File
@@ -49,7 +49,7 @@ ADD server-ce/config/env.sh /etc/overleaf/env.sh
# Configure nginx # Configure nginx
# --------------- # ---------------
ADD server-ce/nginx/nginx.conf.template /etc/nginx/templates/nginx.conf.template ADD server-ce/nginx/nginx.conf.template /etc/nginx/templates/nginx.conf.template
ADD server-ce/nginx/overleaf.conf /etc/nginx/sites-enabled/overleaf.conf ADD server-ce/nginx/overleaf.conf.template /etc/nginx/templates/overleaf.conf.template
ADD server-ce/nginx/clsi-nginx.conf /etc/nginx/sites-enabled/clsi-nginx.conf ADD server-ce/nginx/clsi-nginx.conf /etc/nginx/sites-enabled/clsi-nginx.conf
@@ -121,7 +121,9 @@ ENV GRACEFUL_SHUTDOWN_DELAY_SECONDS=1
ENV NODE_ENV="production" ENV NODE_ENV="production"
ENV LOG_LEVEL="info" ENV LOG_LEVEL="info"
ENV GIT_BRIDGE_HOST=git-bridge
ENV GIT_BRIDGE_PORT=8000
ENV GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE=52428800
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["/sbin/my_init"] ENTRYPOINT ["/sbin/my_init"]
+2
View File
@@ -37,6 +37,7 @@ build-base:
--cache-from $(OVERLEAF_BASE_BRANCH) \ --cache-from $(OVERLEAF_BASE_BRANCH) \
--tag $(OVERLEAF_BASE_TAG) \ --tag $(OVERLEAF_BASE_TAG) \
--tag $(OVERLEAF_BASE_BRANCH) \ --tag $(OVERLEAF_BASE_BRANCH) \
--network=host \
$(MONOREPO_ROOT) $(MONOREPO_ROOT)
@@ -52,6 +53,7 @@ build-community:
--file Dockerfile \ --file Dockerfile \
--tag $(OVERLEAF_TAG) \ --tag $(OVERLEAF_TAG) \
--tag $(OVERLEAF_BRANCH) \ --tag $(OVERLEAF_BRANCH) \
--network=host \
$(MONOREPO_ROOT) $(MONOREPO_ROOT)
push: push:
+1
View File
@@ -5,6 +5,7 @@ export DOCUMENT_UPDATER_HOST=127.0.0.1
export DOCUPDATER_HOST=127.0.0.1 export DOCUPDATER_HOST=127.0.0.1
export FILESTORE_HOST=127.0.0.1 export FILESTORE_HOST=127.0.0.1
export HISTORY_V1_HOST=127.0.0.1 export HISTORY_V1_HOST=127.0.0.1
export LINKED_URL_PROXY_HOST=127.0.0.1
export NOTIFICATIONS_HOST=127.0.0.1 export NOTIFICATIONS_HOST=127.0.0.1
export PROJECT_HISTORY_HOST=127.0.0.1 export PROJECT_HISTORY_HOST=127.0.0.1
export REALTIME_HOST=127.0.0.1 export REALTIME_HOST=127.0.0.1
+12 -1
View File
@@ -193,6 +193,8 @@ const settings = {
maintenanceMessage: process.env.OVERLEAF_MAINTENANCE_MESSAGE, maintenanceMessage: process.env.OVERLEAF_MAINTENANCE_MESSAGE,
maintenanceMessageHTML: process.env.OVERLEAF_MAINTENANCE_MESSAGE_HTML, maintenanceMessageHTML: process.env.OVERLEAF_MAINTENANCE_MESSAGE_HTML,
enableRegiserPage: process.env.OVERLEAF_ENABLE_REGISTER_PAGE === 'true',
// The name this is used to describe your Overleaf Community Edition Installation // The name this is used to describe your Overleaf Community Edition Installation
appName: process.env.OVERLEAF_APP_NAME || 'Overleaf Community Edition', appName: process.env.OVERLEAF_APP_NAME || 'Overleaf Community Edition',
@@ -393,7 +395,16 @@ if (process.env.OVERLEAF_EMAIL_FROM_ADDRESS != null) {
} }
if ( if (
process.env.OVERLEAF_EMAIL_SMTP_USER != null || process.env.OVERLEAF_EMAIL_SMTP_USER != null &&
process.env.OVERLEAF_EMAIL_SMTP_PASS_FILE != null
) {
settings.email.parameters.auth = {
user: process.env.OVERLEAF_EMAIL_SMTP_USER,
pass: require("fs").readFileSync(process.env.OVERLEAF_EMAIL_SMTP_PASS_FILE, "utf8")
}
}
else if (
process.env.OVERLEAF_EMAIL_SMTP_USER != null &&
process.env.OVERLEAF_EMAIL_SMTP_PASS != null process.env.OVERLEAF_EMAIL_SMTP_PASS != null
) { ) {
settings.email.parameters.auth = { settings.email.parameters.auth = {
+13
View File
@@ -0,0 +1,13 @@
diff --git a/services/web/modules/authentication/saml/app/src/SAMLModuleManager.mjs b/services/web/modules/authentication/saml/app/src/SAMLModuleManager.mjs
index 29e9ae52cd..335ace8209 100644
--- a/services/web/modules/authentication/saml/app/src/SAMLModuleManager.mjs
+++ b/services/web/modules/authentication/saml/app/src/SAMLModuleManager.mjs
@@ -26,7 +26,7 @@ const SAMLModuleManager = {
callbackUrl: `${Settings.siteUrl.replace(/\/+$/, '')}/saml/login/callback`,
issuer: process.env.OVERLEAF_SAML_ISSUER,
audience: process.env.OVERLEAF_SAML_AUDIENCE,
- cert: readFilesContentFromEnv(process.env.OVERLEAF_SAML_IDP_CERT),
+ idpCert: readFilesContentFromEnv(process.env.OVERLEAF_SAML_IDP_CERT),
privateKey: readFilesContentFromEnv(process.env.OVERLEAF_SAML_PRIVATE_KEY),
decryptionPvk: readFilesContentFromEnv(process.env.OVERLEAF_SAML_DECRYPTION_PVK),
signatureAlgorithm: process.env.OVERLEAF_SAML_SIGNATURE_ALGORITHM,
+20
View File
@@ -0,0 +1,20 @@
FROM overleafcep/sharelatex:5.5.6-ext-v3.2
# add normalize and sanitize to linked-url-proxy
COPY *.patch* .
RUN bash -ec 'for p in *.patch; do echo "=== Applying $p ==="; patch -p1 < "$p" && rm $p; done' \
&& npm install als-normalize-urlpath@2.3.0 \
&& rm -f ./package.json.orig \
./services/history-v1/storage/scripts/back_fill_file_hash.mjs.orig \
./services/history-v1/test/acceptance/js/storage/back_fill_file_hash.test.mjs.orig \
./services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs.orig \
./services/web/config/settings.defaults.js.orig \
./services/web/frontend/js/features/source-editor/components/toolbar/toolbar-items.tsx.orig \
./services/web/test/frontend/features/share-project-modal/components/share-project-modal.test.jsx.orig \
./services/web/package.json.orig \
./services/filestore/app.js.orig \
./package-lock.json.orig \
./package-lock.json.diff \
/etc/my_init.d/100_make_overleaf_data_dirs.sh.orig \
/etc/overleaf/settings.js.orig
@@ -0,0 +1,256 @@
diff --git a/package.json b/package.json
index 64fbd258ed..7cc3c7f9b3 100644
--- a/package.json
+++ b/package.json
@@ -66,6 +66,7 @@
"services/history-v1",
"services/idp",
"services/latexqc",
+ "services/linked-url-proxy",
"services/notifications",
"services/project-history",
"services/real-time",
diff --git a/services/linked-url-proxy/Dockerfile b/services/linked-url-proxy/Dockerfile
index 677006182c..7f2053f614 100644
--- a/services/linked-url-proxy/Dockerfile
+++ b/services/linked-url-proxy/Dockerfile
@@ -2,7 +2,7 @@
# Instead run bin/update_build_scripts from
# https://github.com/overleaf/internal/
-FROM node:20.18.2 AS base
+FROM node:22.18.0 AS base
WORKDIR /overleaf/services/linked-url-proxy
@@ -13,16 +13,20 @@ RUN mkdir /home/node/.config && chown node:node /home/node/.config
FROM base AS app
COPY package.json package-lock.json /overleaf/
+COPY libraries/fetch-utils/package.json /overleaf/libraries/fetch-utils/package.json
COPY libraries/logger/package.json /overleaf/libraries/logger/package.json
COPY libraries/metrics/package.json /overleaf/libraries/metrics/package.json
+COPY libraries/o-error/package.json /overleaf/libraries/o-error/package.json
COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
COPY services/linked-url-proxy/package.json /overleaf/services/linked-url-proxy/package.json
COPY patches/ /overleaf/patches/
RUN cd /overleaf && npm ci --quiet
+COPY libraries/fetch-utils/ /overleaf/libraries/fetch-utils/
COPY libraries/logger/ /overleaf/libraries/logger/
COPY libraries/metrics/ /overleaf/libraries/metrics/
+COPY libraries/o-error/ /overleaf/libraries/o-error/
COPY libraries/settings/ /overleaf/libraries/settings/
COPY services/linked-url-proxy/ /overleaf/services/linked-url-proxy/
diff --git a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
index 1269eede0d..ea82f65d16 100644
--- a/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
+++ b/services/linked-url-proxy/app/js/LinkedUrlProxyController.mjs
@@ -1,4 +1,6 @@
import dns from 'dns/promises'
+import { sanitizeUrl } from 'strict-url-sanitise'
+import normalizeUrlPath from 'als-normalize-urlpath'
import ipaddr from 'ipaddr.js'
import { URL } from 'node:url'
import { Transform } from 'node:stream'
@@ -18,15 +20,7 @@ function isBlockedIp(ipStr, targetUrl) {
}
const range = addr.range()
- if ([
- 'loopback',
- 'private',
- 'linkLocal',
- 'multicast',
- 'reserved',
- 'broadcast',
- 'unspecified'
- ].includes(range)) {
+ if (!['unicast'].includes(addr.range())) {
return true
}
@@ -44,7 +38,7 @@ function isBlockedIp(ipStr, targetUrl) {
return false
}
-async function validateSourceUrl(hostname, targetUrl) {
+async function checkUrlAccess(hostname, targetUrl) {
const records = await dns.lookup(hostname, { all: true }).catch(() => [])
if (!records.length) {
const err = new Error(`DNS lookup failed for ${hostname}`)
@@ -62,22 +56,40 @@ async function validateSourceUrl(hostname, targetUrl) {
}
}
-async function fetchValidated(urlStr, redirectCount = 0) {
+async function validateAndFetch(rawUrl, redirectCount = 0) {
if (redirectCount > Settings.maxRedirects) {
const err = new Error('Too many redirects')
err.info = { status: 421 }
throw err
}
- const url = new URL(urlStr)
+ const sanitizedUrl = sanitizeUrl(rawUrl)
+ if (!sanitizedUrl) {
+ const err = new Error(`Invalid or unsafe URL: ${rawUrl}`)
+ err.info = { status: 400 }
+ throw err
+ }
+
+ const url = new URL(sanitizedUrl)
+
if (!['http:', 'https:'].includes(url.protocol)) {
const err = new Error(`${url.protocol} protocol is not allowed`)
err.info = { status: 400 }
throw err
}
- // Validate DNS and blocked IPs
- await validateSourceUrl(url.hostname, urlStr)
+ const normalizedPath = normalizeUrlPath(url.pathname).pathname
+
+ if (!normalizedPath) {
+ const err = new Error(`Invalid or unsafe URL path: ${url.pathname}`)
+ err.info = { status: 400 }
+ throw err
+ }
+
+ const normalizedUrl = url.toString()
+
+ // check DNS and allowed resources
+ await checkUrlAccess(url.hostname, normalizedUrl)
const opts = {
redirect: 'manual',
@@ -86,7 +98,7 @@ async function fetchValidated(urlStr, redirectCount = 0) {
}
try {
- const { stream, response } = await fetchStreamWithResponse(urlStr, opts)
+ const { stream, response } = await fetchStreamWithResponse(normalizedUrl, opts)
const contentLengthHeader = response.headers.get('content-length')
if (contentLengthHeader) {
@@ -112,8 +124,8 @@ async function fetchValidated(urlStr, redirectCount = 0) {
if (status >= 300 && status < 400) {
const location = err.response.headers.get('Location')
if (location) {
- const nextUrl = new URL(location, url).toString()
- return fetchValidated(nextUrl, redirectCount + 1)
+ const nextUrl = new URL(location, normalizedUrl).toString()
+ return validateAndFetch(nextUrl, redirectCount + 1)
} else {
const e = new Error('Redirect response missing Location header')
e.info = { status: 421 }
@@ -141,14 +153,14 @@ async function proxy(req, res) {
return
}
- const { stream: upstreamStream, response, headers } = await fetchValidated(targetUrl)
+ const { stream: upstreamStream, response, headers } = await validateAndFetch(targetUrl)
res.statusCode = response.status || 200
res.setHeader('Content-Type', headers['content-type'] || 'application/octet-stream')
res.setHeader('Cache-Control', 'no-store')
function onError(err) {
- logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
+ logger.info({ err, url: req.url }, 'linked-url-proxy request failed')
try { upstreamStream.destroy() } catch (_) {}
if (!res.headersSent) {
let body = `Error: ${err?.message ?? String(err)}`
@@ -163,11 +175,9 @@ async function proxy(req, res) {
upstreamStream.pipe(res)
} catch (err) {
- logger.warn({ err, url: req.url }, 'linked-url-proxy request failed')
-
- let status = err.info.status
+ const status = err.info?.status || 500
+ logger.info({ linkedUrl: err.message, status, url: req.url }, 'linked-url-proxy request failed')
let body = `Error: ${err.message || String(err)}`
-
try {
res.writeHead(status, { 'Content-Type': 'text/plain' })
res.end(body)
diff --git a/services/linked-url-proxy/package.json b/services/linked-url-proxy/package.json
index 845551c8ef..8081a80822 100644
--- a/services/linked-url-proxy/package.json
+++ b/services/linked-url-proxy/package.json
@@ -2,18 +2,24 @@
"name": "@overleaf/linked-url-proxy",
"description": "An API for providing linked url proxy",
"private": true,
- "type": "module",
"main": "app.mjs",
"scripts": {
- "start": "node app.mjs"
+ "start": "node app.mjs",
+ "nodemon": "node --watch app.mjs"
},
- "version": "0.1.0",
+ "version": "0.1.1",
"dependencies": {
"@overleaf/settings": "*",
"@overleaf/logger": "*",
"@overleaf/metrics": "*",
"async": "^3.2.5",
- "express": "^4.21.2"
- "ipaddr.js": "^1.9.1"
+ "express": "^4.21.2",
+ "ipaddr.js": "^1.9.1",
+ "als-normalize-urlpath": "^2.3.0",
+ "strict-url-sanitise": "^0.0.1"
+ },
+ "devDependencies": {
+ "als-normalize-urlpath": "^2.3.0",
+ "strict-url-sanitise": "^0.0.1"
}
}
diff --git a/services/linked-url-proxy/tsconfig.json b/services/linked-url-proxy/tsconfig.json
index c018d6e682..0639496cc8 100644
--- a/services/linked-url-proxy/tsconfig.json
+++ b/services/linked-url-proxy/tsconfig.json
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.backend.json",
"include": [
- "app.js",
+ "app.mjs",
"app.ts",
"app/js/**/*",
"benchmarks/**/*",
diff --git a/services/web/package.json b/services/web/package.json
index 21da1188a2..c2c4fcfaba 100644
--- a/services/web/package.json
+++ b/services/web/package.json
@@ -69,6 +69,7 @@
"last 1 year",
"safari > 14"
],
+
"dependencies": {
"@contentful/rich-text-html-renderer": "^16.0.2",
"@contentful/rich-text-types": "^16.0.2",
@@ -92,6 +93,7 @@
"@xmldom/xmldom": "^0.7.13",
"accepts": "^1.3.7",
"ajv": "^8.12.0",
+ "als-normalize-urlpath": "^2.3.0",
"archiver": "^5.3.0",
"async": "^3.2.5",
"base-x": "^4.0.1",
@@ -166,7 +168,8 @@
"request": "^2.88.2",
"requestretry": "^7.1.0",
"sanitize-html": "^2.8.1",
- "stripe": "^18.1.0",
+ "strict-url-sanitise": "^0.0.1",
+ "stripe": "^18.4.0",
"tough-cookie": "^4.0.0",
"tsscmp": "^1.0.6",
"uid-safe": "^2.1.5",
@@ -16,11 +16,18 @@ fi
nginx_template_file="${nginx_templates_dir}/nginx.conf.template" nginx_template_file="${nginx_templates_dir}/nginx.conf.template"
nginx_config_file="${nginx_dir}/nginx.conf" nginx_config_file="${nginx_dir}/nginx.conf"
overleaf_template_file="${nginx_templates_dir}/overleaf.conf.template"
overleaf_config_file="${nginx_dir}/sites-enabled/overleaf.conf"
if [ -f "${nginx_template_file}" ]; then if [ -f "${nginx_template_file}" ]; then
export NGINX_KEEPALIVE_TIMEOUT="${NGINX_KEEPALIVE_TIMEOUT:-65}" export NGINX_KEEPALIVE_TIMEOUT="${NGINX_KEEPALIVE_TIMEOUT:-65}"
export NGINX_WORKER_CONNECTIONS="${NGINX_WORKER_CONNECTIONS:-768}" export NGINX_WORKER_CONNECTIONS="${NGINX_WORKER_CONNECTIONS:-768}"
export NGINX_WORKER_PROCESSES="${NGINX_WORKER_PROCESSES:-4}" export NGINX_WORKER_PROCESSES="${NGINX_WORKER_PROCESSES:-4}"
export MAX_UPLOAD_SIZE="${MAX_UPLOAD_SIZE:-50}"
export GIT_BRIDGE_HOST="${GIT_BRIDGE_HOST:-git-bridge}"
export GIT_BRIDGE_PORT="${GIT_BRIDGE_PORT:-8000}"
export GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE="${GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE:-52428800}"
echo "Nginx: generating config file from template" echo "Nginx: generating config file from template"
# Note the single-quotes, they are important. # Note the single-quotes, they are important.
@@ -31,10 +38,20 @@ if [ -f "${nginx_template_file}" ]; then
${NGINX_KEEPALIVE_TIMEOUT} ${NGINX_KEEPALIVE_TIMEOUT}
${NGINX_WORKER_CONNECTIONS} ${NGINX_WORKER_CONNECTIONS}
${NGINX_WORKER_PROCESSES} ${NGINX_WORKER_PROCESSES}
${MAX_UPLOAD_SIZE}
' \ ' \
< "${nginx_template_file}" \ < "${nginx_template_file}" \
> "${nginx_config_file}" > "${nginx_config_file}"
# Config git-bridge in nginx
envsubst '
${GIT_BRIDGE_HOST}
${GIT_BRIDGE_PORT}
${GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE}
' \
< "${overleaf_template_file}" \
> "${overleaf_config_file}"
echo "Checking Nginx config" echo "Checking Nginx config"
nginx -t nginx -t
+2 -2
View File
@@ -34,8 +34,8 @@ server {
root /var/lib/overleaf/data/output/$1-$2/generated-files/$3/; root /var/lib/overleaf/data/output/$1-$2/generated-files/$3/;
} }
# handle output files for anonymous users # handle output files for anonymous users
location ~ ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ { location ~ ^/project/([0-9a-f-]+)/build/([0-9a-f-]+)/output/(.+)$ {
rewrite ^/project/([0-9a-f]+)/build/([0-9a-f-]+)/output/(.+)$ /$3 break; rewrite ^/project/([0-9a-f-]+)/build/([0-9a-f-]+)/output/(.+)$ /$3 break;
root /var/lib/overleaf/data/output/$1/generated-files/$2/; root /var/lib/overleaf/data/output/$1/generated-files/$2/;
} }
+1 -1
View File
@@ -46,7 +46,7 @@ http {
gzip_disable "msie6"; gzip_disable "msie6";
gzip_proxied any; # allow upstream server to compress. gzip_proxied any; # allow upstream server to compress.
client_max_body_size 50m; client_max_body_size ${MAX_UPLOAD_SIZE}m;
# gzip_vary on; # gzip_vary on;
# gzip_proxied any; # gzip_proxied any;
@@ -21,6 +21,23 @@ server {
proxy_send_timeout 10m; proxy_send_timeout 10m;
} }
location ^~ /git/ {
resolver_timeout 2s;
resolver 127.0.0.11 valid=10s;
set $git_upstream http://${GIT_BRIDGE_HOST}:${GIT_BRIDGE_PORT};
rewrite ^/git/(.*)$ /$1 break;
client_max_body_size ${GIT_BRIDGE_REPOSTORE_MAX_FILE_SIZE};
proxy_pass $git_upstream;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 10m;
proxy_send_timeout 10m;
}
location /socket.io { location /socket.io {
proxy_pass http://127.0.0.1:3026; proxy_pass http://127.0.0.1:3026;
proxy_http_version 1.1; proxy_http_version 1.1;
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
NODE_PARAMS=""
if [ "$DEBUG_NODE" == "true" ]; then
echo "running debug - linked-url-proxy"
NODE_PARAMS="--inspect=0.0.0.0:30660"
fi
source /etc/overleaf/env.sh
export LISTEN_ADDRESS=127.0.0.1
exec /sbin/setuser www-data /usr/bin/node $NODE_PARAMS /overleaf/services/linked-url-proxy/app.mjs >> /var/log/overleaf/linked-url-proxy.log 2>&1
+3
View File
@@ -29,6 +29,9 @@ module.exports = [
{ {
name: 'history-v1', name: 'history-v1',
}, },
{
name: 'linked-url-proxy',
},
] ]
if (require.main === module) { if (require.main === module) {
+1 -1
View File
@@ -19,7 +19,7 @@ services:
OVERLEAF_EMAIL_SMTP_HOST: "mailtrap" OVERLEAF_EMAIL_SMTP_HOST: "mailtrap"
OVERLEAF_EMAIL_SMTP_PORT: "25" OVERLEAF_EMAIL_SMTP_PORT: "25"
OVERLEAF_EMAIL_SMTP_IGNORE_TLS: "true" OVERLEAF_EMAIL_SMTP_IGNORE_TLS: "true"
ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file" ENABLED_LINKED_FILE_TYPES: "project_file,project_output_file,zotero"
ENABLE_CONVERSIONS: "true" ENABLE_CONVERSIONS: "true"
EMAIL_CONFIRMATION_DISABLED: "true" EMAIL_CONFIRMATION_DISABLED: "true"
OVERLEAF_INVITE_TOKEN_SECRET: "invite-token-secret" OVERLEAF_INVITE_TOKEN_SECRET: "invite-token-secret"
+120
View File
@@ -2,6 +2,8 @@ import {
createNewFile, createNewFile,
createProjectAndOpenInNewEditor, createProjectAndOpenInNewEditor,
openProjectById, openProjectById,
openProjectByName,
shareProjectByEmailAndAcceptInviteViaDash,
testNewFileUpload, testNewFileUpload,
} from './helpers/project' } from './helpers/project'
import { isExcludedBySharding, startWith } from './helpers/config' import { isExcludedBySharding, startWith } from './helpers/config'
@@ -321,6 +323,124 @@ describe('editor', function () {
}) })
}) })
describe('editing mode', function () {
if (isExcludedBySharding('PRO_DEFAULT_1')) return
startWith({ pro: true })
const OWNER = USER
const EDITOR = COLLABORATOR
const REVIEWER = 'reviewer@example.com'
const VIEWER = 'viewer@example.com'
ensureUserExists({ email: OWNER })
ensureUserExists({ email: EDITOR })
ensureUserExists({ email: REVIEWER })
ensureUserExists({ email: VIEWER })
let projectName: string
function openEditingModeMenu() {
cy.findByRole('button', { name: 'View' }).click()
cy.findByRole('menuitem', { name: 'Editing mode' }).click()
}
beforeWithReRunOnTestRetry(() => {
projectName = `project-${uuid()}`
login(OWNER)
createProjectAndOpenInNewEditor(projectName, { type: 'Example project' })
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
EDITOR,
'Editor',
true
)
login(OWNER)
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
REVIEWER,
'Reviewer',
true
)
login(OWNER)
shareProjectByEmailAndAcceptInviteViaDash(
projectName,
VIEWER,
'Viewer',
true
)
})
it('lets an editor switch between editing and reviewing', function () {
login(EDITOR)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Editing' })
.should('have.attr', 'aria-selected', 'true')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Reviewing' })
.should('have.attr', 'aria-selected', 'false')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Viewing' }).should('not.exist')
cy.findByRole('menuitem', { name: 'Reviewing' }).click()
cy.findByRole('button', { name: 'Reviewing' }).should('exist')
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Reviewing' }).should(
'have.attr',
'aria-selected',
'true'
)
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-selected',
'false'
)
cy.findByRole('menuitem', { name: 'Editing' }).click()
cy.findByRole('button', { name: 'Editing' }).should('exist')
})
it('shows a reviewer in reviewing mode with editing disabled', function () {
login(REVIEWER)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Reviewing' })
.should('have.attr', 'aria-selected', 'true')
.and('not.have.attr', 'aria-disabled')
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-disabled',
'true'
)
cy.findByRole('menuitem', { name: 'Viewing' }).should('not.exist')
})
it('shows a viewer in viewing mode with editing and reviewing disabled', function () {
login(VIEWER)
openProjectByName(projectName, true)
openEditingModeMenu()
cy.findByRole('menuitem', { name: 'Viewing' }).should(
'have.attr',
'aria-selected',
'true'
)
cy.findByRole('menuitem', { name: 'Editing' }).should(
'have.attr',
'aria-disabled',
'true'
)
cy.findByRole('menuitem', { name: 'Reviewing' }).should(
'have.attr',
'aria-disabled',
'true'
)
})
})
function createRandomLetterString() { function createRandomLetterString() {
const chars = 'abcdefghijklmnopqrstuvwxyz' const chars = 'abcdefghijklmnopqrstuvwxyz'
let result = '' let result = ''
+6 -1
View File
@@ -256,7 +256,12 @@ describe('filestore migration', function () {
}) })
} else { } else {
// 5.x // 5.x
startWith({ version: '5.5.5', pro: true, withDataDir: true }) startWith({
version: '5.5.5',
pro: true,
withDataDir: true,
resetData: true,
})
defaultImage = 'frog.jpg' defaultImage = 'frog.jpg'
ensureUserExists({ email }) ensureUserExists({ email })
before(function () { before(function () {
+2 -2
View File
@@ -206,7 +206,7 @@ export function openProjectViaInviteNotification(
function shareProjectByEmail( function shareProjectByEmail(
projectName: string, projectName: string,
email: string, email: string,
level: 'Viewer' | 'Editor', level: 'Viewer' | 'Editor' | 'Reviewer',
newEditor: boolean = false newEditor: boolean = false
) { ) {
openProjectByName(projectName, newEditor) openProjectByName(projectName, newEditor)
@@ -231,7 +231,7 @@ function shareProjectByEmail(
export function shareProjectByEmailAndAcceptInviteViaDash( export function shareProjectByEmailAndAcceptInviteViaDash(
projectName: string, projectName: string,
email: string, email: string,
level: 'Viewer' | 'Editor', level: 'Viewer' | 'Editor' | 'Reviewer',
newEditor: boolean = false newEditor: boolean = false
) { ) {
shareProjectByEmail(projectName, email, level, newEditor) shareProjectByEmail(projectName, email, level, newEditor)
+1 -1
View File
@@ -21,8 +21,8 @@ import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry
describe('Project Sharing', function () { describe('Project Sharing', function () {
if (isExcludedBySharding('PRO_CUSTOM_4')) return if (isExcludedBySharding('PRO_CUSTOM_4')) return
startWith({ withDataDir: true, pro: true, resetData: true })
ensureUserExists({ email: 'user@example.com' }) ensureUserExists({ email: 'user@example.com' })
startWith({ withDataDir: true, pro: true })
let projectName: string let projectName: string
let recompile: () => void let recompile: () => void
+6 -9
View File
@@ -2,10 +2,10 @@
# Instead run bin/update_build_scripts from # Instead run bin/update_build_scripts from
# https://github.com/overleaf/internal/ # https://github.com/overleaf/internal/
BUILD_NUMBER ?= local export BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) export BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96) export BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96)
PROJECT_NAME = chat export PROJECT_NAME = chat
BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
HERE=$(shell pwd) HERE=$(shell pwd)
export MONOREPO ?= $(shell cd ../../ && pwd) export MONOREPO ?= $(shell cd ../../ && pwd)
@@ -28,13 +28,10 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
$(MONOREPO)/tools/migrations/package.json \ $(MONOREPO)/tools/migrations/package.json \
$(MONOREPO)/.yarn/patches/* \ $(MONOREPO)/.yarn/patches/* \
| sha256sum | cut -d '-' -f1) | sha256sum | cut -d '-' -f1)
export MOCHA_GREP ?=
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ DOCKER_COMPOSE := docker compose ${DOCKER_COMPOSE_FLAGS}
BRANCH_NAME=$(BRANCH_NAME_TAG_SAFE) \
PROJECT_NAME=$(PROJECT_NAME) \
MOCHA_GREP=${MOCHA_GREP} \
docker compose ${DOCKER_COMPOSE_FLAGS}
# Common flags for docker compose run. # Common flags for docker compose run.
# --rm Tear down test runner container # --rm Tear down test runner container
-1
View File
@@ -5,6 +5,5 @@ chat
--env-pass-through= --env-pass-through=
--esmock-loader=False --esmock-loader=False
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/chat
--pipeline-owner=44 --pipeline-owner=44
--public-repo=False --public-repo=False
+3 -3
View File
@@ -4,7 +4,7 @@
services: services:
test_unit: test_unit:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
user: node user: node
volumes: volumes:
- ./reports:/overleaf/services/chat/reports - ./reports:/overleaf/services/chat/reports
@@ -21,7 +21,7 @@ services:
condition: service_started condition: service_started
test_acceptance: test_acceptance:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
environment: environment:
CI: CI:
RETRIES: RETRIES:
@@ -42,7 +42,7 @@ services:
tar: tar:
build: . build: .
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
volumes: volumes:
- ./:/tmp/build/ - ./:/tmp/build/
command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
@@ -5,6 +5,6 @@ before('run migrations', async function () {
this.timeout(60_000) this.timeout(60_000)
await promisify(exec)( await promisify(exec)(
`cd ../../tools/migrations && npm run migrations -- migrate -t server-ce` `cd ../../tools/migrations && yarn run migrations migrate -t 'server-ce & !nonblocking'`
) )
}) })
+2 -1
View File
@@ -1,4 +1,5 @@
compiles compiles
output output
cache cache
uploads uploads
sharded
+6 -9
View File
@@ -2,10 +2,10 @@
# Instead run bin/update_build_scripts from # Instead run bin/update_build_scripts from
# https://github.com/overleaf/internal/ # https://github.com/overleaf/internal/
BUILD_NUMBER ?= local export BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) export BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96) export BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96)
PROJECT_NAME = clsi export PROJECT_NAME = clsi
BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
HERE=$(shell pwd) HERE=$(shell pwd)
export MONOREPO ?= $(shell cd ../../ && pwd) export MONOREPO ?= $(shell cd ../../ && pwd)
@@ -29,13 +29,10 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
$(MONOREPO)/services/clsi/package.json \ $(MONOREPO)/services/clsi/package.json \
$(MONOREPO)/.yarn/patches/* \ $(MONOREPO)/.yarn/patches/* \
| sha256sum | cut -d '-' -f1) | sha256sum | cut -d '-' -f1)
export MOCHA_GREP ?=
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ DOCKER_COMPOSE := docker compose ${DOCKER_COMPOSE_FLAGS}
BRANCH_NAME=$(BRANCH_NAME_TAG_SAFE) \
PROJECT_NAME=$(PROJECT_NAME) \
MOCHA_GREP=${MOCHA_GREP} \
docker compose ${DOCKER_COMPOSE_FLAGS}
# Common flags for docker compose run. # Common flags for docker compose run.
# --rm Tear down test runner container # --rm Tear down test runner container
+1 -1
View File
@@ -29,7 +29,7 @@ The CLSI can be configured through the following environment variables:
- `PROCESS_LIFE_SPAN_LIMIT_MS` - Process life span limit in milliseconds - `PROCESS_LIFE_SPAN_LIMIT_MS` - Process life span limit in milliseconds
- `SMOKE_TEST` - Whether to run smoke tests - `SMOKE_TEST` - Whether to run smoke tests
- `TEXLIVE_IMAGE` - The TeX Live Docker image to use for sibling containers, e.g. `us-east1-docker.pkg.dev/overleaf-ops/ol-docker/texlive-full:2025.1` - `TEXLIVE_IMAGE` - The TeX Live Docker image to use for sibling containers, e.g. `us-east1-docker.pkg.dev/overleaf-ops/ol-docker/texlive-full:2025.1`
- `TEX_LIVE_IMAGE_NAME_OVERRIDE` - The name of the registry for the Docker image e.g. `us-east1-docker.pkg.dev/overleaf-ops/ol-docker` - `TEX_LIVE_DOCKER_IMAGE_ROOT` - The name of the registry for the Docker image e.g. `us-east1-docker.pkg.dev/overleaf-ops/ol-docker`
- `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TeX Live image. Defaults to `tex` - `TEXLIVE_IMAGE_USER` - When using sibling containers, the user to run as in the TeX Live image. Defaults to `tex`
- `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for TeX Live (see the `\openout` primitive [documentation](http://tug.org/texinfohtml/web2c.html#tex-invocation)) - `TEXLIVE_OPENOUT_ANY` - Sets the `openout_any` environment variable for TeX Live (see the `\openout` primitive [documentation](http://tug.org/texinfohtml/web2c.html#tex-invocation))
+1 -1
View File
@@ -10,7 +10,7 @@ import logger from '@overleaf/logger'
let commandRunnerPath let commandRunnerPath
if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) { if ((Settings.clsi != null ? Settings.clsi.dockerRunner : undefined) === true) {
commandRunnerPath = './DockerRunner.js' commandRunnerPath = './DockerRunner.mjs'
} else { } else {
commandRunnerPath = './LocalCommandRunner.js' commandRunnerPath = './LocalCommandRunner.js'
} }
+5
View File
@@ -192,6 +192,11 @@ async function doCompile(request, stats, timings) {
} }
} }
// Pass through checkpoint setting
if (request.enableCheckpoint) {
env.ENABLE_CHECKPOINT = '1'
}
const compileStart = Date.now() const compileStart = Date.now()
const compileName = getCompileName(request.project_id, request.user_id) const compileName = getCompileName(request.project_id, request.user_id)
@@ -173,6 +173,7 @@ const LATEX_EXPORT_CONFIGS = {
'--to', '--to',
'html', 'html',
'--standalone', '--standalone',
'--mathml',
], ],
}, },
} }
+110
View File
@@ -0,0 +1,110 @@
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
import logger from '@overleaf/logger'
let LockManager
const LockState = {} // locks for docker container operations, by container name
export default LockManager = {
MAX_LOCK_HOLD_TIME: 15000, // how long we can keep a lock
MAX_LOCK_WAIT_TIME: 10000, // how long we wait for a lock
LOCK_TEST_INTERVAL: 1000, // retry time
tryLock(key, callback) {
let lockValue
if (callback == null) {
callback = function () {}
}
const existingLock = LockState[key]
if (existingLock != null) {
// the lock is already taken, check how old it is
const lockAge = Date.now() - existingLock.created
if (lockAge < LockManager.MAX_LOCK_HOLD_TIME) {
return callback(null, false) // we didn't get the lock, bail out
} else {
logger.error(
{ key, lock: existingLock, age: lockAge },
'taking old lock by force'
)
}
}
// take the lock
LockState[key] = lockValue = { created: Date.now() }
return callback(null, true, lockValue)
},
getLock(key, callback) {
let attempt
if (callback == null) {
callback = function () {}
}
const startTime = Date.now()
return (attempt = () =>
LockManager.tryLock(key, function (error, gotLock, lockValue) {
if (error != null) {
return callback(error)
}
if (gotLock) {
return callback(null, lockValue)
} else if (Date.now() - startTime > LockManager.MAX_LOCK_WAIT_TIME) {
const e = new Error('Lock timeout')
e.key = key
return callback(e)
} else {
return setTimeout(attempt, LockManager.LOCK_TEST_INTERVAL)
}
}))()
},
releaseLock(key, lockValue, callback) {
if (callback == null) {
callback = function () {}
}
const existingLock = LockState[key]
if (existingLock === lockValue) {
// lockValue is an object, so we can test by reference
delete LockState[key] // our lock, so we can free it
return callback()
} else if (existingLock != null) {
// lock exists but doesn't match ours
logger.error(
{ key, lock: existingLock },
'tried to release lock taken by force'
)
return callback()
} else {
logger.error(
{ key, lock: existingLock },
'tried to release lock that has gone'
)
return callback()
}
},
runWithLock(key, runner, callback) {
if (callback == null) {
callback = function () {}
}
return LockManager.getLock(key, function (error, lockValue) {
if (error != null) {
return callback(error)
}
return runner((error1, ...args) =>
LockManager.releaseLock(key, lockValue, function (error2) {
error = error1 || error2
if (error != null) {
return callback(error)
}
return callback(null, ...Array.from(args))
})
)
})
},
}
+630
View File
@@ -0,0 +1,630 @@
import { promisify } from 'node:util'
import Settings from '@overleaf/settings'
import logger from '@overleaf/logger'
import Docker from 'dockerode'
import crypto from 'node:crypto'
import async from 'async'
import LockManager from './DockerLockManager.js'
import Path from 'node:path'
import _ from 'lodash'
import { getLastProjectAccessTime } from './LastProjectAccess.js'
const dockerode = new Docker()
const ONE_HOUR_IN_MS = 60 * 60 * 1000
logger.debug('using docker runner')
let containerMonitorTimeout
let containerMonitorInterval
const DockerRunner = {
run(
projectId,
command,
directory,
image,
timeout,
environment,
compileGroup,
cwd,
callback
) {
command = command.map(arg =>
arg.toString().replace('$COMPILE_DIR', '/compile')
)
if (image == null) {
image = Settings.clsi.docker.image
}
if (
Settings.clsi.docker.allowedImages &&
!Settings.clsi.docker.allowedImages.includes(image)
) {
return callback(new Error('image not allowed'))
}
if (Settings.texliveImageNameOveride != null) {
const img = Path.basename(image)
image = `${Settings.texliveImageNameOveride}/${img}`
}
if (compileGroup === 'synctex-output') {
// In: directory = '/overleaf/services/clsi/output/projectId-userId/generated-files/buildId'
// directory.split('/').slice(-3) === 'projectId-userId/generated-files/buildId'
// sandboxedCompilesHostDirOutput = '/host/output'
// Out: directory = '/host/output/projectId-userId/generated-files/buildId'
directory = Path.join(
Settings.path.sandboxedCompilesHostDirOutput,
...directory.split('/').slice(-3)
)
} else {
// In: directory = '/overleaf/services/clsi/compiles/projectId-userId'
// Path.basename(directory) === 'projectId-userId'
// sandboxedCompilesHostDirCompiles = '/host/compiles'
// Out: directory = '/host/compiles/projectId-userId'
directory = Path.join(
Settings.path.sandboxedCompilesHostDirCompiles,
Path.basename(directory)
)
}
const volumes = { [directory]: '/compile' }
if (
compileGroup === 'synctex' ||
compileGroup === 'synctex-output' ||
compileGroup === 'wordcount'
) {
volumes[directory] += ':ro'
}
const options = DockerRunner._getContainerOptions(
command,
image,
volumes,
timeout,
environment,
compileGroup,
cwd
)
const fingerprint = DockerRunner._fingerprintContainer(options)
const name = `project-${projectId}-${fingerprint}`
options.name = name
// logOptions = _.clone(options)
// logOptions?.HostConfig?.SecurityOpt = "secomp used, removed in logging"
logger.debug({ projectId }, 'running docker container')
DockerRunner._runAndWaitForContainer(
options,
volumes,
timeout,
(error, output) => {
if (error && error.statusCode === 500) {
logger.debug(
{ err: error, projectId },
'error running container so destroying and retrying'
)
DockerRunner.destroyContainer(name, null, true, error => {
if (error != null) {
return callback(error)
}
DockerRunner._runAndWaitForContainer(
options,
volumes,
timeout,
callback
)
})
} else {
callback(error, output)
}
}
)
// pass back the container name to allow it to be killed
return name
},
kill(containerId, callback) {
logger.debug({ containerId }, 'sending kill signal to container')
const container = dockerode.getContainer(containerId)
container.kill(error => {
if (
error != null &&
error.message != null &&
error.message.match(/Cannot kill container .* is not running/)
) {
logger.warn(
{ err: error, containerId },
'container not running, continuing'
)
error = null
}
if (error != null) {
logger.error({ err: error, containerId }, 'error killing container')
callback(error)
} else {
callback()
}
})
},
_runAndWaitForContainer(options, volumes, timeout, _callback) {
const callback = _.once(_callback)
const { name } = options
let streamEnded = false
let containerReturned = false
let output = {}
function callbackIfFinished() {
if (streamEnded && containerReturned) {
callback(null, output)
}
}
function attachStreamHandler(error, _output) {
if (error != null) {
return callback(error)
}
output = _output
streamEnded = true
callbackIfFinished()
}
DockerRunner.startContainer(
options,
volumes,
attachStreamHandler,
(error, containerId) => {
if (error != null) {
return callback(error)
}
DockerRunner.waitForContainer(
name,
timeout,
options,
(error, exitCode) => {
if (error != null) {
return callback(error)
}
if (exitCode === 137) {
// exit status from kill -9
const err = new Error('terminated')
err.terminated = true
return callback(err)
}
if (exitCode === 1) {
// exit status from chktex
const err = new Error('exited')
err.code = exitCode
return callback(err)
}
output.exitCode = exitCode
containerReturned = true
logger.debug(
// The seccomp policy is very large. Avoid logging it. _.omit deep clones.
{ exitCode, options: _.omit(options, 'HostConfig.SecurityOpt') },
'docker container has exited'
)
callbackIfFinished()
}
)
}
)
},
_getContainerOptions(
command,
image,
volumes,
timeout,
environment,
compileGroup,
cwd
) {
const timeoutInSeconds = timeout / 1000
for (const hostVol in volumes) {
const dockerVol = volumes[hostVol]
if (volumes[hostVol].slice(-3).indexOf(':r') === -1) {
volumes[hostVol] = `${dockerVol}:rw`
}
}
// merge settings and environment parameter
const env = {}
for (const src of [Settings.clsi.docker.env, environment || {}]) {
for (const key in src) {
const value = src[key]
env[key] = value
}
}
// set the path based on the image year
const match = image.match(/:([0-9]+)\.[0-9]+|:TL([0-9]+)/)
// the rolling build does not follow our <year>.<version>.<patch> convention
const year = match ? match[1] || match[2] : 'rolling'
env.PATH = `/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/texlive/${year}/bin/x86_64-linux/`
const options = {
Cmd: command,
Image: image,
WorkingDir: cwd
? Path.posix.join('/compile', cwd)
: '/compile',
NetworkDisabled: true,
Memory: 1024 * 1024 * 1024 * 1024, // 1 Gb
User: Settings.clsi.docker.user,
Env: Object.entries(env).map(([key, value]) => `${key}=${value}`),
HostConfig: {
Binds: Object.entries(volumes).map(
([hostVol, dockerVol]) => `${hostVol}:${dockerVol}`
),
LogConfig: { Type: 'none', Config: {} },
Ulimits: [
{
Name: 'cpu',
Soft: timeoutInSeconds + 5,
Hard: timeoutInSeconds + 10,
},
],
CapDrop: ['ALL'],
SecurityOpt: ['no-new-privileges'],
},
}
if (Settings.clsi.docker.seccomp_profile != null) {
options.HostConfig.SecurityOpt.push(
`seccomp=${Settings.clsi.docker.seccomp_profile}`
)
}
if (Settings.clsi.docker.apparmor_profile != null) {
options.HostConfig.SecurityOpt.push(
`apparmor=${Settings.clsi.docker.apparmor_profile}`
)
}
if (Settings.clsi.docker.runtime) {
options.HostConfig.Runtime = Settings.clsi.docker.runtime
}
if (Settings.clsi.docker.Readonly) {
options.HostConfig.ReadonlyRootfs = true
options.HostConfig.Tmpfs = { '/tmp': 'rw,noexec,nosuid,size=65536k' }
options.Volumes['/home/tex'] = {}
}
// Allow per-compile group overriding of individual settings
if (
Settings.clsi.docker.compileGroupConfig &&
Settings.clsi.docker.compileGroupConfig[compileGroup]
) {
const override = Settings.clsi.docker.compileGroupConfig[compileGroup]
for (const key in override) {
_.set(options, key, override[key])
}
}
return options
},
_fingerprintContainer(containerOptions) {
// Yay, Hashing!
const json = JSON.stringify(containerOptions)
return crypto.createHash('md5').update(json).digest('hex')
},
startContainer(options, volumes, attachStreamHandler, callback) {
LockManager.runWithLock(
options.name,
releaseLock =>
DockerRunner._startContainer(
options,
volumes,
attachStreamHandler,
releaseLock
),
callback
)
},
// Check that volumes exist and are directories
_startContainer(options, volumes, attachStreamHandler, callback) {
callback = _.once(callback)
const { name } = options
logger.debug({ containerName: name }, 'starting container')
const container = dockerode.getContainer(name)
function createAndStartContainer() {
dockerode.createContainer(options, (error, container) => {
if (error != null) {
return callback(error)
}
startExistingContainer()
})
}
function startExistingContainer() {
DockerRunner.attachToContainer(
options.name,
attachStreamHandler,
error => {
if (error != null) {
return callback(error)
}
container.start(error => {
if (error != null && error.statusCode !== 304) {
callback(error)
} else {
// already running
callback()
}
})
}
)
}
container.inspect((error, stats) => {
if (error != null && error.statusCode === 404) {
createAndStartContainer()
} else if (error != null) {
logger.err(
{ containerName: name, error },
'unable to inspect container to start'
)
callback(error)
} else {
startExistingContainer()
}
})
},
attachToContainer(containerId, attachStreamHandler, attachStartCallback) {
const container = dockerode.getContainer(containerId)
container.attach({ stdout: 1, stderr: 1, stream: 1 }, (error, stream) => {
if (error != null) {
logger.error(
{ err: error, containerId },
'error attaching to container'
)
return attachStartCallback(error)
} else {
attachStartCallback()
}
logger.debug({ containerId }, 'attached to container')
const MAX_OUTPUT = 1024 * 1024 * 2 // limit output to 2MB
function createStringOutputStream(name) {
return {
data: '',
overflowed: false,
write(data) {
if (this.overflowed) {
return
}
if (this.data.length < MAX_OUTPUT) {
this.data += data
} else {
logger.info(
{
containerId,
length: this.data.length,
maxLen: MAX_OUTPUT,
},
`${name} exceeds max size`
)
this.data += `(...truncated at ${MAX_OUTPUT} chars...)`
this.overflowed = true
}
},
// kill container if too much output
// docker.containers.kill(containerId, () ->)
}
}
const stdout = createStringOutputStream('stdout')
const stderr = createStringOutputStream('stderr')
container.modem.demuxStream(stream, stdout, stderr)
stream.on('error', err =>
logger.error(
{ err, containerId },
'error reading from container stream'
)
)
stream.on('end', () =>
attachStreamHandler(null, { stdout: stdout.data, stderr: stderr.data })
)
})
},
waitForContainer(containerId, timeout, options, _callback) {
const callback = _.once(_callback)
const container = dockerode.getContainer(containerId)
let timedOut = false
const timeoutId = setTimeout(() => {
timedOut = true
logger.debug({ containerId }, 'timeout reached, killing container')
container.kill(err => {
logger.warn({ err, containerId }, 'failed to kill container')
})
}, timeout)
logger.debug({ containerId }, 'waiting for docker container')
container.wait((error, res) => {
if (error?.statusCode === 404 && options.HostConfig.AutoRemove) {
logger.debug(
{ containerId },
'auto-destroy container destroyed before starting to wait'
)
clearTimeout(timeoutId)
return callback(null, 0)
}
if (error != null) {
clearTimeout(timeoutId)
logger.warn({ err: error, containerId }, 'error waiting for container')
return callback(error)
}
if (timedOut) {
logger.debug({ containerId }, 'docker container timed out')
error = new Error('container timed out')
error.timedout = true
callback(error)
} else {
clearTimeout(timeoutId)
logger.debug(
{ containerId, exitCode: res.StatusCode },
'docker container returned'
)
callback(null, res.StatusCode)
}
})
},
destroyContainer(containerName, containerId, shouldForce, callback) {
// We want the containerName for the lock and, ideally, the
// containerId to delete. There is a bug in the docker.io module
// where if you delete by name and there is an error, it throws an
// async exception, but if you delete by id it just does a normal
// error callback. We fall back to deleting by name if no id is
// supplied.
LockManager.runWithLock(
containerName,
releaseLock =>
DockerRunner._destroyContainer(
containerId || containerName,
shouldForce,
releaseLock
),
callback
)
},
_destroyContainer(containerId, shouldForce, callback) {
logger.debug({ containerId }, 'destroying docker container')
const container = dockerode.getContainer(containerId)
container.remove({ force: shouldForce === true, v: true }, error => {
if (error != null && error.statusCode === 404) {
logger.warn(
{ err: error, containerId },
'container not found, continuing'
)
error = null
}
if (error != null) {
logger.error({ err: error, containerId }, 'error destroying container')
} else {
logger.debug({ containerId }, 'destroyed container')
}
callback(error)
})
},
// handle expiry of docker containers
MAX_CONTAINER_AGE: Settings.clsi.docker.maxContainerAge || ONE_HOUR_IN_MS,
examineOldContainer(container, callback) {
const name = container.Name || (container.Names && container.Names[0])
const created = container.Created * 1000 // creation time is returned in seconds
const now = Date.now()
const age = now - created
const maxAge = DockerRunner.MAX_CONTAINER_AGE
const ttl = maxAge - age
logger.debug(
{ containerName: name, created, now, age, maxAge, ttl },
'checking whether to destroy container'
)
return { name, id: container.Id, ttl }
},
destroyOldContainers(callback) {
dockerode.listContainers({ all: true }, (error, containers) => {
if (error != null) {
return callback(error)
}
const jobs = []
for (const container of containers) {
const { name, id, ttl } = DockerRunner.examineOldContainer(container)
if (name.slice(0, 9) === '/project-' && ttl <= 0) {
// extract projectId: /project-<projectId>-...
const match = name.match(/^\/project-([a-f0-9]{24})-/)
const projectId = match && match[1]
if (projectId != null) {
const lastAccess = getLastProjectAccessTime(projectId)
// if last access time is missing, lastAccess === 0, container will be destroyed
// provided that MAX_CONTAINER_AGE is less that 56 years, but anyway, let's check it
if (lastAccess && lastAccess > Date.now() - DockerRunner.MAX_CONTAINER_AGE) {
continue
}
}
// strip the / prefix
// the LockManager uses the plain container name
const plainName = name.slice(1)
jobs.push(cb =>
DockerRunner.destroyContainer(plainName, id, false, () => cb())
)
}
}
// Ignore errors because some containers get stuck but
// will be destroyed next time
async.series(jobs, callback)
})
},
startContainerMonitor() {
logger.debug(
{ maxAge: DockerRunner.MAX_CONTAINER_AGE },
'starting container expiry'
)
// guarantee only one monitor is running
DockerRunner.stopContainerMonitor()
// randomise the start time
const randomDelay = Math.floor(Math.random() * 5 * 60 * 1000)
containerMonitorTimeout = setTimeout(() => {
containerMonitorInterval = setInterval(
() =>
DockerRunner.destroyOldContainers(err => {
if (err) {
logger.error({ err }, 'failed to destroy old containers')
}
}),
ONE_HOUR_IN_MS
)
}, randomDelay)
},
stopContainerMonitor() {
if (containerMonitorTimeout) {
clearTimeout(containerMonitorTimeout)
containerMonitorTimeout = undefined
}
if (containerMonitorInterval) {
clearInterval(containerMonitorInterval)
containerMonitorInterval = undefined
}
},
canRunSyncTeXInOutputDir() {
return Boolean(Settings.path.sandboxedCompilesHostDirOutput)
},
}
DockerRunner.startContainerMonitor()
DockerRunner.promises = {
run: promisify(DockerRunner.run),
kill: promisify(DockerRunner.kill),
}
export default DockerRunner
+5
View File
@@ -65,6 +65,11 @@ function parse(body, callback) {
type: 'number', type: 'number',
} }
) )
response.enableCheckpoint = _parseAttribute(
'enableCheckpoint',
compile.options.enableCheckpoint,
{ default: false, type: 'boolean' }
)
response.timeout = _parseAttribute('timeout', compile.options.timeout, { response.timeout = _parseAttribute('timeout', compile.options.timeout, {
default: MAX_TIMEOUT, default: MAX_TIMEOUT,
type: 'number', type: 'number',
+10
View File
@@ -23,9 +23,15 @@ import Metrics from '@overleaf/metrics'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import settings from '@overleaf/settings' import settings from '@overleaf/settings'
import ClsiMetrics from './Metrics.js' import ClsiMetrics from './Metrics.js'
import { Minimatch } from 'minimatch'
const { shouldSkipMetrics } = ClsiMetrics const { shouldSkipMetrics } = ClsiMetrics
// Additional file patterns to keep between compiles
const preciousFileMatcher = new Minimatch(settings.preciousFilePattern, {
dot: true,
})
let ResourceWriter let ResourceWriter
const parallelFileDownloads = settings.parallelFileDownloads || 1 const parallelFileDownloads = settings.parallelFileDownloads || 1
@@ -265,6 +271,10 @@ export default ResourceWriter = {
// Epstopdf generated files // Epstopdf generated files
shouldDelete = false shouldDelete = false
} }
// Keep additional precious files and directories
if (shouldDelete && preciousFileMatcher.match(path)) {
shouldDelete = false
}
if ( if (
path === 'output.tar.gz' || path === 'output.tar.gz' ||
path === 'output.synctex.gz' || path === 'output.synctex.gz' ||
+1 -2
View File
@@ -1,11 +1,10 @@
clsi clsi
--data-dirs=cache,compiles,output --data-dirs=cache,compiles,output
--dependencies= --dependencies=
--env-add=DOWNLOAD_HOST=http://clsi-nginx:8080,ALLOWED_COMPILE_GROUPS=clsi-perf simple-latex-file,ENABLE_PDF_CACHING=true,PDF_CACHING_ENABLE_WORKER_POOL=true,ALLOWED_IMAGES=quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02,TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_IMAGE_NAME_OVERRIDE=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER=tex,SANDBOXED_COMPILES=true,SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output,ENABLE_PANDOC_CONVERSIONS=true,ENABLE_PDF_CONVERSIONS=true --env-add=DOWNLOAD_HOST=http://clsi-nginx:8080,ALLOWED_COMPILE_GROUPS=clsi-perf simple-latex-file,ENABLE_PDF_CACHING=true,PDF_CACHING_ENABLE_WORKER_POOL=true,ALLOWED_IMAGES=quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02,TEXLIVE_IMAGE=quay.io/sharelatex/texlive-full:2025.1,TEX_LIVE_DOCKER_IMAGE_ROOT=us-east1-docker.pkg.dev/overleaf-ops/ol-docker,TEXLIVE_IMAGE_USER=tex,SANDBOXED_COMPILES=true,SANDBOXED_COMPILES_HOST_DIR_COMPILES=$PWD/compiles,SANDBOXED_COMPILES_HOST_DIR_OUTPUT=$PWD/output,ENABLE_PANDOC_CONVERSIONS=true,ENABLE_PDF_CONVERSIONS=true
--env-pass-through= --env-pass-through=
--esmock-loader=False --esmock-loader=False
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/clsi
--pipeline-owner=32 --pipeline-owner=32
--public-repo=True --public-repo=True
--test-unit-vitest=True --test-unit-vitest=True
+15 -4
View File
@@ -35,6 +35,7 @@ module.exports = {
process.env.PDFTOCAIRO_IMAGE || 'quay.io/sharelatex/pdftocairo:24.02', process.env.PDFTOCAIRO_IMAGE || 'quay.io/sharelatex/pdftocairo:24.02',
enablePdfConversions: process.env.ENABLE_PDF_CONVERSIONS === 'true', enablePdfConversions: process.env.ENABLE_PDF_CONVERSIONS === 'true',
maxUploadSize: 50 * 1024 * 1024, maxUploadSize: 50 * 1024 * 1024,
preciousFilePattern: process.env.PRECIOUS_FILE_PATTERN || '',
internal: { internal: {
clsi: { clsi: {
@@ -92,7 +93,7 @@ module.exports = {
parallelFileDownloads: parallelFileDownloads:
parseInt(process.env.FILESTORE_PARALLEL_FILE_DOWNLOADS, 10) || 1, parseInt(process.env.FILESTORE_PARALLEL_FILE_DOWNLOADS, 10) || 1,
filestoreDomainOveride: process.env.FILESTORE_DOMAIN_OVERRIDE, filestoreDomainOveride: process.env.FILESTORE_DOMAIN_OVERRIDE,
texliveImageNameOveride: process.env.TEX_LIVE_IMAGE_NAME_OVERRIDE, texliveImageNameOveride: process.env.TEX_LIVE_DOCKER_IMAGE_ROOT,
texliveOpenoutAny: process.env.TEXLIVE_OPENOUT_ANY, texliveOpenoutAny: process.env.TEXLIVE_OPENOUT_ANY,
texliveMaxPrintLine: process.env.TEXLIVE_MAX_PRINT_LINE, texliveMaxPrintLine: process.env.TEXLIVE_MAX_PRINT_LINE,
enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true', enablePdfCaching: process.env.ENABLE_PDF_CACHING === 'true',
@@ -124,7 +125,7 @@ if (process.env.ALLOWED_COMPILE_GROUPS) {
if ((process.env.DOCKER_RUNNER || process.env.SANDBOXED_COMPILES) === 'true') { if ((process.env.DOCKER_RUNNER || process.env.SANDBOXED_COMPILES) === 'true') {
if ( if (
!fs.existsSync(Path.join(__dirname, '..', 'app', 'js', 'DockerRunner.js')) !fs.existsSync(Path.join(__dirname, '..', 'app', 'js', 'DockerRunner.mjs'))
) { ) {
console.error( console.error(
'Sandboxed compiles are only available with Overleaf Server Pro. Compare Server Pro with Community Edition here: https://docs.overleaf.com/on-premises/welcome/server-pro-vs.-community-edition' 'Sandboxed compiles are only available with Overleaf Server Pro. Compare Server Pro with Community Edition here: https://docs.overleaf.com/on-premises/welcome/server-pro-vs.-community-edition'
@@ -139,13 +140,13 @@ if ((process.env.DOCKER_RUNNER || process.env.SANDBOXED_COMPILES) === 'true') {
image: image:
process.env.TEXLIVE_IMAGE || process.env.TEXLIVE_IMAGE ||
process.env.TEX_LIVE_DOCKER_IMAGE || process.env.TEX_LIVE_DOCKER_IMAGE ||
'quay.io/sharelatex/texlive-full:2017.1', process.env.ALL_TEX_LIVE_DOCKER_IMAGES.split(',')[0].trim(),
env: { env: {
HOME: '/tmp', HOME: '/tmp',
CLSI: 1, CLSI: 1,
}, },
socketPath: '/var/run/docker.sock', socketPath: '/var/run/docker.sock',
user: process.env.TEXLIVE_IMAGE_USER || 'tex', user: process.env.TEXLIVE_IMAGE_USER || 'www-data',
}, },
optimiseInDocker: true, optimiseInDocker: true,
expireProjectAfterIdleMs: 24 * 60 * 60 * 1000, expireProjectAfterIdleMs: 24 * 60 * 60 * 1000,
@@ -202,6 +203,16 @@ if ((process.env.DOCKER_RUNNER || process.env.SANDBOXED_COMPILES) === 'true') {
} }
} }
if (process.env.NEW_APPARMOR_PROFILE) {
try {
module.exports.clsi.docker.new_apparmor_profile =
process.env.NEW_APPARMOR_PROFILE
} catch (error) {
console.error(error, 'could not apply new apparmor profile setting')
process.exit(1)
}
}
if (process.env.ALLOWED_IMAGES) { if (process.env.ALLOWED_IMAGES) {
try { try {
module.exports.clsi.docker.allowedImages = module.exports.clsi.docker.allowedImages =
+4 -4
View File
@@ -4,7 +4,7 @@
services: services:
test_unit: test_unit:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
volumes: volumes:
- ./reports:/overleaf/services/clsi/reports - ./reports:/overleaf/services/clsi/reports
- ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json - ../../tsconfig.backend.json:/overleaf/tsconfig.backend.json
@@ -17,7 +17,7 @@ services:
VITEST_NO_CACHE: true VITEST_NO_CACHE: true
test_acceptance: test_acceptance:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
environment: environment:
CI: CI:
RETRIES: RETRIES:
@@ -32,7 +32,7 @@ services:
PDF_CACHING_ENABLE_WORKER_POOL: true PDF_CACHING_ENABLE_WORKER_POOL: true
ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02 ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02
TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1 TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1
TEX_LIVE_IMAGE_NAME_OVERRIDE: us-east1-docker.pkg.dev/overleaf-ops/ol-docker TEX_LIVE_DOCKER_IMAGE_ROOT: us-east1-docker.pkg.dev/overleaf-ops/ol-docker
TEXLIVE_IMAGE_USER: tex TEXLIVE_IMAGE_USER: tex
SANDBOXED_COMPILES: true SANDBOXED_COMPILES: true
SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles
@@ -51,7 +51,7 @@ services:
tar: tar:
build: . build: .
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
volumes: volumes:
- ./:/tmp/build/ - ./:/tmp/build/
command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
+1 -1
View File
@@ -55,7 +55,7 @@ services:
PDF_CACHING_ENABLE_WORKER_POOL: true PDF_CACHING_ENABLE_WORKER_POOL: true
ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02 ALLOWED_IMAGES: quay.io/sharelatex/texlive-full:2017.1 quay.io/sharelatex/texlive-full:2025.1 quay.io/sharelatex/pandoc:3.9 quay.io/sharelatex/pdftocairo:24.02
TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1 TEXLIVE_IMAGE: quay.io/sharelatex/texlive-full:2025.1
TEX_LIVE_IMAGE_NAME_OVERRIDE: us-east1-docker.pkg.dev/overleaf-ops/ol-docker TEX_LIVE_DOCKER_IMAGE_ROOT: us-east1-docker.pkg.dev/overleaf-ops/ol-docker
TEXLIVE_IMAGE_USER: tex TEXLIVE_IMAGE_USER: tex
SANDBOXED_COMPILES: true SANDBOXED_COMPILES: true
SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles SANDBOXED_COMPILES_HOST_DIR_COMPILES: $PWD/compiles
+1
View File
@@ -31,6 +31,7 @@
"dockerode": "^4.0.9", "dockerode": "^4.0.9",
"express": "4.22.1", "express": "4.22.1",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"minimatch": "^10.2.5",
"multer": "2.1.1", "multer": "2.1.1",
"overleaf-editor-core": "workspace:*", "overleaf-editor-core": "workspace:*",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
+914
View File
@@ -0,0 +1,914 @@
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_X32",
"SCMP_ARCH_AARCH64",
"SCMP_ARCH_ARM"
],
"syscalls": [
{
"name": "getrandom",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "access",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "arch_prctl",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "brk",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "chdir",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "chmod",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "clock_getres",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "clock_gettime",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "clock_nanosleep",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "clone",
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 2080505856,
"valueTwo": 0,
"op": "SCMP_CMP_MASKED_EQ"
}
]
},
{
"name": "clone3",
"action": "SCMP_ACT_ERRNO",
"errnoRet": 38
},
{
"name": "close",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "copy_file_range",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "creat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "dup",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "dup2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "dup3",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "execve",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "execveat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "exit",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "exit_group",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "faccessat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "faccessat2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fadvise64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fadvise64_64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fallocate",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fchdir",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fchmod",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fchmodat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fcntl",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fcntl64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fdatasync",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fork",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fstat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fstat64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fstatat64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fstatfs",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fstatfs64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fsync",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "ftruncate",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "ftruncate64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "futex",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "futimesat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getcpu",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getcwd",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getdents",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getdents64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getegid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getegid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "geteuid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "geteuid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getgid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getgid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getgroups",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getgroups32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getpgid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getpgrp",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getpid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getppid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getpriority",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getresgid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getresgid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getresuid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getresuid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getrlimit",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "get_robust_list",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getrusage",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getsid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "gettid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getuid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "getuid32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "ioctl",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "kill",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "_llseek",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "lseek",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "lstat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "lstat64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "madvise",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mkdir",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mkdirat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mmap",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mmap2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mprotect",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "mremap",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "munmap",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "newfstatat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "open",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "openat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "openat2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pause",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pipe",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pipe2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "prctl",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pread64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "preadv",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "prlimit64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pwrite64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pwritev",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "read",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "readlink",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "readlinkat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "readv",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rename",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "renameat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "renameat2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "restart_syscall",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rmdir",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigaction",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigpending",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigprocmask",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigqueueinfo",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigreturn",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigsuspend",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_sigtimedwait",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "rt_tgsigqueueinfo",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_getaffinity",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_getparam",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_get_priority_max",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_get_priority_min",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_getscheduler",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_rr_get_interval",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sched_yield",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sendfile",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sendfile64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "setgroups",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "setgroups32",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "set_robust_list",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "set_tid_address",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sigaltstack",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "stat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "statx",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "stat64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "statfs",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "statfs64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sync",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sync_file_range",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "syncfs",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "sysinfo",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "tgkill",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timer_create",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timer_delete",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timer_getoverrun",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timer_gettime",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timer_settime",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "times",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "tkill",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "truncate",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "truncate64",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "umask",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "uname",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "unlink",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "unlinkat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "utime",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "utimensat",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "utimes",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "vfork",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "vhangup",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "wait4",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "waitid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "write",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "writev",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "pread",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "setgid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "setuid",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "capget",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "capset",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "fchown",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "gettimeofday",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_wait",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_pwait",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_pwait2",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "poll",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "ppoll",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_create",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_create1",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "epoll_ctl",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timerfd_create",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "timerfd_settime",
"action": "SCMP_ACT_ALLOW",
"args": []
},
{
"name": "eventfd2",
"action": "SCMP_ACT_ALLOW",
"args": []
}
]
}
@@ -91,6 +91,7 @@ const LATEX_TO_DOCUMENT_CASES = [
'--to', '--to',
'html', 'html',
'--standalone', '--standalone',
'--mathml',
'--resource-path=..', '--resource-path=..',
'--extract-media=.', '--extract-media=.',
], ],
@@ -0,0 +1,259 @@
import { vi, describe, beforeEach, it } from 'vitest'
import sinon from 'sinon'
import path from 'node:path'
const modulePath = path.join(
import.meta.dirname,
'../../../app/js/DockerLockManager'
)
describe('DockerLockManager', function () {
beforeEach(async function (ctx) {
vi.doMock('@overleaf/settings', () => ({
default: (ctx.Settings = { clsi: { docker: {} } }),
}))
return (ctx.LockManager = (await import(modulePath)).default)
})
return describe('runWithLock', function () {
describe('with a single lock', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.callback = sinon.stub()
return ctx.LockManager.runWithLock(
'lock-one',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world'), 100),
(err, ...args) => {
ctx.callback(err, ...Array.from(args))
return resolve()
}
)
})
})
return it('should call the callback', function (ctx) {
return ctx.callback
.calledWith(null, 'hello', 'world')
.should.equal(true)
})
})
describe('with two locks', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.callback1 = sinon.stub()
ctx.callback2 = sinon.stub()
ctx.LockManager.runWithLock(
'lock-one',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'one'), 100),
(err, ...args) => {
return ctx.callback1(err, ...Array.from(args))
}
)
return ctx.LockManager.runWithLock(
'lock-two',
releaseLock =>
setTimeout(() => releaseLock(null, 'hello', 'world', 'two'), 200),
(err, ...args) => {
ctx.callback2(err, ...Array.from(args))
return resolve()
}
)
})
})
it('should call the first callback', function (ctx) {
return ctx.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function (ctx) {
return ctx.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
return describe('with lock contention', function () {
describe('where the first lock is released quickly', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
ctx.LockManager.MAX_LOCK_WAIT_TIME = 1000
ctx.LockManager.LOCK_TEST_INTERVAL = 100
ctx.callback1 = sinon.stub()
ctx.callback2 = sinon.stub()
ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'one'),
100
),
(err, ...args) => {
return ctx.callback1(err, ...Array.from(args))
}
)
return ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'two'),
200
),
(err, ...args) => {
ctx.callback2(err, ...Array.from(args))
return resolve()
}
)
})
})
it('should call the first callback', function (ctx) {
return ctx.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function (ctx) {
return ctx.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
describe('where the first lock is held longer than the waiting time', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
let doneTwo
ctx.LockManager.MAX_LOCK_HOLD_TIME = 10000
ctx.LockManager.MAX_LOCK_WAIT_TIME = 1000
ctx.LockManager.LOCK_TEST_INTERVAL = 100
ctx.callback1 = sinon.stub()
ctx.callback2 = sinon.stub()
let doneOne = (doneTwo = false)
const finish = function (key) {
if (key === 1) {
doneOne = true
}
if (key === 2) {
doneTwo = true
}
if (doneOne && doneTwo) {
return resolve()
}
}
ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'one'),
1100
),
(err, ...args) => {
ctx.callback1(err, ...Array.from(args))
return finish(1)
}
)
return ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'two'),
100
),
(err, ...args) => {
ctx.callback2(err, ...Array.from(args))
return finish(2)
}
)
})
})
it('should call the first callback', function (ctx) {
return ctx.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback with an error', function (ctx) {
const error = sinon.match.instanceOf(Error)
return ctx.callback2.calledWith(error).should.equal(true)
})
})
return describe('where the first lock is held longer than the max holding time', function () {
beforeEach(async function (ctx) {
await new Promise((resolve, reject) => {
let doneTwo
ctx.LockManager.MAX_LOCK_HOLD_TIME = 1000
ctx.LockManager.MAX_LOCK_WAIT_TIME = 2000
ctx.LockManager.LOCK_TEST_INTERVAL = 100
ctx.callback1 = sinon.stub()
ctx.callback2 = sinon.stub()
let doneOne = (doneTwo = false)
const finish = function (key) {
if (key === 1) {
doneOne = true
}
if (key === 2) {
doneTwo = true
}
if (doneOne && doneTwo) {
return resolve()
}
}
ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'one'),
1500
),
(err, ...args) => {
ctx.callback1(err, ...Array.from(args))
return finish(1)
}
)
return ctx.LockManager.runWithLock(
'lock',
releaseLock =>
setTimeout(
() => releaseLock(null, 'hello', 'world', 'two'),
100
),
(err, ...args) => {
ctx.callback2(err, ...Array.from(args))
return finish(2)
}
)
})
})
it('should call the first callback', function (ctx) {
return ctx.callback1
.calledWith(null, 'hello', 'world', 'one')
.should.equal(true)
})
return it('should call the second callback', function (ctx) {
return ctx.callback2
.calledWith(null, 'hello', 'world', 'two')
.should.equal(true)
})
})
})
})
})
@@ -5,7 +5,7 @@ import Path from 'node:path'
const modulePath = Path.join( const modulePath = Path.join(
import.meta.dirname, import.meta.dirname,
'../../../app/js/DockerRunner' '../../../app/js/DockerRunner.mjs'
) )
describe('DockerRunner', () => { describe('DockerRunner', () => {
@@ -19,6 +19,12 @@ describe('ResourceWriter', () => {
}), }),
})) }))
vi.doMock('@overleaf/settings', () => ({
default: (ctx.settings = {
preciousFilePattern: '{keepdir/**,.other/keepdir/**}',
}),
}))
vi.doMock('../../../app/js/ResourceStateManager', () => ({ vi.doMock('../../../app/js/ResourceStateManager', () => ({
default: (ctx.ResourceStateManager = {}), default: (ctx.ResourceStateManager = {}),
})) }))
@@ -246,6 +252,12 @@ describe('ResourceWriter', () => {
{ {
path: 'output.stderr', path: 'output.stderr',
}, },
{
path: 'keepdir/file',
},
{
path: '.other/keepdir/.subdir/file',
},
] ]
ctx.resources = 'mock-resources' ctx.resources = 'mock-resources'
ctx.request = { ctx.request = {
@@ -297,6 +309,18 @@ describe('ResourceWriter', () => {
.should.equal(true) .should.equal(true)
}) })
it('should not delete additionally specified files', ctx => {
return ctx.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(ctx.basePath, 'keepdir/file'))
.should.equal(false)
})
it('should not delete additionally specified hidden files', ctx => {
return ctx.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(ctx.basePath, '.other/keepdir/.subdir/file'))
.should.equal(false)
})
it('should not delete the extra aux files', ctx => { it('should not delete the extra aux files', ctx => {
return ctx.ResourceWriter._deleteFileIfNotDirectory return ctx.ResourceWriter._deleteFileIfNotDirectory
.calledWith(path.join(ctx.basePath, 'extra.aux')) .calledWith(path.join(ctx.basePath, 'extra.aux'))
+2
View File
@@ -29,6 +29,7 @@ COPY libraries/object-persistor/package.json /overleaf/libraries/object-persisto
COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/package.json COPY libraries/promise-utils/package.json /overleaf/libraries/promise-utils/package.json
COPY libraries/settings/package.json /overleaf/libraries/settings/package.json COPY libraries/settings/package.json /overleaf/libraries/settings/package.json
COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json COPY libraries/stream-utils/package.json /overleaf/libraries/stream-utils/package.json
COPY libraries/validation-tools/package.json /overleaf/libraries/validation-tools/package.json
COPY services/docstore/package.json /overleaf/services/docstore/package.json COPY services/docstore/package.json /overleaf/services/docstore/package.json
COPY tools/migrations/package.json /overleaf/tools/migrations/package.json COPY tools/migrations/package.json /overleaf/tools/migrations/package.json
COPY .yarn/patches/ /overleaf/.yarn/patches/ COPY .yarn/patches/ /overleaf/.yarn/patches/
@@ -45,6 +46,7 @@ COPY libraries/object-persistor/ /overleaf/libraries/object-persistor/
COPY libraries/promise-utils/ /overleaf/libraries/promise-utils/ COPY libraries/promise-utils/ /overleaf/libraries/promise-utils/
COPY libraries/settings/ /overleaf/libraries/settings/ COPY libraries/settings/ /overleaf/libraries/settings/
COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/ COPY libraries/stream-utils/ /overleaf/libraries/stream-utils/
COPY libraries/validation-tools/ /overleaf/libraries/validation-tools/
COPY services/docstore/ /overleaf/services/docstore/ COPY services/docstore/ /overleaf/services/docstore/
COPY tools/migrations/ /overleaf/tools/migrations/ COPY tools/migrations/ /overleaf/tools/migrations/
+7 -9
View File
@@ -2,10 +2,10 @@
# Instead run bin/update_build_scripts from # Instead run bin/update_build_scripts from
# https://github.com/overleaf/internal/ # https://github.com/overleaf/internal/
BUILD_NUMBER ?= local export BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) export BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96) export BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96)
PROJECT_NAME = docstore export PROJECT_NAME = docstore
BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
HERE=$(shell pwd) HERE=$(shell pwd)
export MONOREPO ?= $(shell cd ../../ && pwd) export MONOREPO ?= $(shell cd ../../ && pwd)
@@ -26,17 +26,15 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
$(MONOREPO)/libraries/promise-utils/package.json \ $(MONOREPO)/libraries/promise-utils/package.json \
$(MONOREPO)/libraries/settings/package.json \ $(MONOREPO)/libraries/settings/package.json \
$(MONOREPO)/libraries/stream-utils/package.json \ $(MONOREPO)/libraries/stream-utils/package.json \
$(MONOREPO)/libraries/validation-tools/package.json \
$(MONOREPO)/services/docstore/package.json \ $(MONOREPO)/services/docstore/package.json \
$(MONOREPO)/tools/migrations/package.json \ $(MONOREPO)/tools/migrations/package.json \
$(MONOREPO)/.yarn/patches/* \ $(MONOREPO)/.yarn/patches/* \
| sha256sum | cut -d '-' -f1) | sha256sum | cut -d '-' -f1)
export MOCHA_GREP ?=
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ DOCKER_COMPOSE := docker compose ${DOCKER_COMPOSE_FLAGS}
BRANCH_NAME=$(BRANCH_NAME_TAG_SAFE) \
PROJECT_NAME=$(PROJECT_NAME) \
MOCHA_GREP=${MOCHA_GREP} \
docker compose ${DOCKER_COMPOSE_FLAGS}
# Common flags for docker compose run. # Common flags for docker compose run.
# --rm Tear down test runner container # --rm Tear down test runner container
+2 -13
View File
@@ -7,11 +7,7 @@ import Settings from '@overleaf/settings'
import logger from '@overleaf/logger' import logger from '@overleaf/logger'
import express from 'express' import express from 'express'
import bodyParser from 'body-parser' import bodyParser from 'body-parser'
import { import { handleValidationError } from '@overleaf/validation-tools'
celebrate as validate,
Joi,
errors as handleValidationErrors,
} from 'celebrate'
import mongodb from './app/js/mongodb.js' import mongodb from './app/js/mongodb.js'
import Errors from './app/js/Errors.js' import Errors from './app/js/Errors.js'
import HttpController from './app/js/HttpController.js' import HttpController from './app/js/HttpController.js'
@@ -79,13 +75,6 @@ app.post(
app.patch( app.patch(
'/project/:project_id/doc/:doc_id', '/project/:project_id/doc/:doc_id',
bodyParser.json(), bodyParser.json(),
validate({
body: {
deleted: Joi.boolean(),
name: Joi.string().when('deleted', { is: true, then: Joi.required() }),
deletedAt: Joi.date().when('deleted', { is: true, then: Joi.required() }),
},
}),
HttpController.patchDoc HttpController.patchDoc
) )
app.delete('/project/:project_id/doc/:doc_id', (req, res) => { app.delete('/project/:project_id/doc/:doc_id', (req, res) => {
@@ -101,7 +90,7 @@ app.get('/health_check', HttpController.healthCheck)
app.get('/status', (req, res) => res.send('docstore is alive')) app.get('/status', (req, res) => res.send('docstore is alive'))
app.use(handleValidationErrors()) app.use(handleValidationError)
app.use(function (error, req, res, next) { app.use(function (error, req, res, next) {
if (error instanceof Errors.NotFoundError) { if (error instanceof Errors.NotFoundError) {
logger.warn({ req }, 'not found') logger.warn({ req }, 'not found')
+12 -10
View File
@@ -5,6 +5,17 @@ import HealthChecker from './HealthChecker.js'
import Errors from './Errors.js' import Errors from './Errors.js'
import Settings from '@overleaf/settings' import Settings from '@overleaf/settings'
import { expressify } from '@overleaf/promise-utils' import { expressify } from '@overleaf/promise-utils'
import { parseReq, z } from '@overleaf/validation-tools'
const patchDocSchema = z.object({
body: z
.object({
deleted: z.literal(true),
deletedAt: z.coerce.date(),
name: z.string(),
})
.strict(),
})
async function getDoc(req, res) { async function getDoc(req, res) {
const { doc_id: docId, project_id: projectId } = req.params const { doc_id: docId, project_id: projectId } = req.params
@@ -182,18 +193,9 @@ async function updateDoc(req, res) {
} }
async function patchDoc(req, res) { async function patchDoc(req, res) {
const { body: meta } = parseReq(req, patchDocSchema)
const { doc_id: docId, project_id: projectId } = req.params const { doc_id: docId, project_id: projectId } = req.params
logger.debug({ projectId, docId }, 'patching doc') logger.debug({ projectId, docId }, 'patching doc')
const allowedFields = ['deleted', 'deletedAt', 'name']
const meta = {}
Object.entries(req.body).forEach(([field, value]) => {
if (allowedFields.includes(field)) {
meta[field] = value
} else {
logger.fatal({ field }, 'joi validation for pathDoc is broken')
}
})
await DocManager.patchDoc(projectId, docId, meta) await DocManager.patchDoc(projectId, docId, meta)
res.sendStatus(204) res.sendStatus(204)
} }
-1
View File
@@ -5,7 +5,6 @@ docstore
--env-pass-through= --env-pass-through=
--esmock-loader=False --esmock-loader=False
--node-version=24.14.1 --node-version=24.14.1
--package-name=@overleaf/docstore
--pipeline-owner=32 --pipeline-owner=32
--public-repo=True --public-repo=True
--test-unit-vitest=True --test-unit-vitest=True
+3 -3
View File
@@ -4,7 +4,7 @@
services: services:
test_unit: test_unit:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
user: node user: node
volumes: volumes:
- ./reports:/overleaf/services/docstore/reports - ./reports:/overleaf/services/docstore/reports
@@ -23,7 +23,7 @@ services:
condition: service_started condition: service_started
test_acceptance: test_acceptance:
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
environment: environment:
CI: CI:
RETRIES: RETRIES:
@@ -49,7 +49,7 @@ services:
tar: tar:
build: . build: .
image: ci/$PROJECT_NAME:$BRANCH_NAME-$BUILD_NUMBER image: ci/$PROJECT_NAME:$BRANCH_NAME_TAG_SAFE-$BUILD_NUMBER
volumes: volumes:
- ./:/tmp/build/ - ./:/tmp/build/
command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs . command: tar -czf /tmp/build/build.tar.gz --exclude=build.tar.gz --exclude-vcs .
+1 -1
View File
@@ -25,10 +25,10 @@
"@overleaf/promise-utils": "workspace:*", "@overleaf/promise-utils": "workspace:*",
"@overleaf/settings": "workspace:*", "@overleaf/settings": "workspace:*",
"@overleaf/stream-utils": "^0.1.0", "@overleaf/stream-utils": "^0.1.0",
"@overleaf/validation-tools": "workspace:*",
"async": "^3.2.5", "async": "^3.2.5",
"body-parser": "1.20.4", "body-parser": "1.20.4",
"bunyan": "^1.8.15", "bunyan": "^1.8.15",
"celebrate": "^15.0.3",
"express": "4.22.1", "express": "4.22.1",
"lodash": "^4.18.1", "lodash": "^4.18.1",
"mongodb-legacy": "6.1.3", "mongodb-legacy": "6.1.3",
@@ -5,6 +5,6 @@ before('run migrations', async function () {
this.timeout(60_000) this.timeout(60_000)
await promisify(exec)( await promisify(exec)(
`cd ../../tools/migrations && npm run migrations -- migrate -t server-ce` `cd ../../tools/migrations && yarn run migrations migrate -t 'server-ce & !nonblocking'`
) )
}) })
@@ -486,7 +486,11 @@ describe('HttpController', () => {
project_id: ctx.projectId, project_id: ctx.projectId,
doc_id: ctx.docId, doc_id: ctx.docId,
} }
ctx.req.body = { name: 'foo.tex' } ctx.req.body = {
deleted: true,
deletedAt: '2026-06-15T00:00:00Z',
name: 'foo.tex',
}
ctx.DocManager.patchDoc = sinon.stub().resolves() ctx.DocManager.patchDoc = sinon.stub().resolves()
await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next) await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next)
}) })
@@ -494,7 +498,12 @@ describe('HttpController', () => {
it('should delete the document', ctx => { it('should delete the document', ctx => {
expect(ctx.DocManager.patchDoc).to.have.been.calledWith( expect(ctx.DocManager.patchDoc).to.have.been.calledWith(
ctx.projectId, ctx.projectId,
ctx.docId ctx.docId,
{
deleted: true,
deletedAt: new Date('2026-06-15T00:00:00Z'),
name: 'foo.tex',
}
) )
}) })
@@ -510,19 +519,13 @@ describe('HttpController', () => {
await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next) await ctx.HttpController.patchDoc(ctx.req, ctx.res, ctx.next)
}) })
it('should log a message', ctx => { it('should pass a validation error to next', ctx => {
expect(ctx.logger.fatal).to.have.been.calledWith( expect(ctx.next).to.have.been.calledOnce
{ field: 'cannot' }, expect(ctx.next.firstCall.args[0].name).to.equal('InvalidRequestError')
'joi validation for pathDoc is broken'
)
}) })
it('should not pass the invalid field along', ctx => { it('should not patch the document', ctx => {
expect(ctx.DocManager.patchDoc).to.have.been.calledWith( expect(ctx.DocManager.patchDoc).not.to.have.been.called
ctx.projectId,
ctx.docId,
{}
)
}) })
}) })
}) })
+6 -9
View File
@@ -2,10 +2,10 @@
# Instead run bin/update_build_scripts from # Instead run bin/update_build_scripts from
# https://github.com/overleaf/internal/ # https://github.com/overleaf/internal/
BUILD_NUMBER ?= local export BUILD_NUMBER ?= local
BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD) export BRANCH_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96) export BRANCH_NAME_TAG_SAFE = $(shell echo $(BRANCH_NAME) | sed 's/\//\-\-/g' | cut -c1-96)
PROJECT_NAME = document-updater export PROJECT_NAME = document-updater
BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]') BUILD_DIR_NAME = $(shell pwd | xargs basename | tr -cd '[a-zA-Z0-9_.\-]')
HERE=$(shell pwd) HERE=$(shell pwd)
export MONOREPO ?= $(shell cd ../../ && pwd) export MONOREPO ?= $(shell cd ../../ && pwd)
@@ -31,13 +31,10 @@ IMAGE_CACHE ?= $(IMAGE_REPO):cache-$(shell cat \
$(MONOREPO)/tools/migrations/package.json \ $(MONOREPO)/tools/migrations/package.json \
$(MONOREPO)/.yarn/patches/* \ $(MONOREPO)/.yarn/patches/* \
| sha256sum | cut -d '-' -f1) | sha256sum | cut -d '-' -f1)
export MOCHA_GREP ?=
DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml DOCKER_COMPOSE_FLAGS ?= -f docker-compose.yml
DOCKER_COMPOSE := BUILD_NUMBER=$(BUILD_NUMBER) \ DOCKER_COMPOSE := docker compose ${DOCKER_COMPOSE_FLAGS}
BRANCH_NAME=$(BRANCH_NAME_TAG_SAFE) \
PROJECT_NAME=$(PROJECT_NAME) \
MOCHA_GREP=${MOCHA_GREP} \
docker compose ${DOCKER_COMPOSE_FLAGS}
# Common flags for docker compose run. # Common flags for docker compose run.
# --rm Tear down test runner container # --rm Tear down test runner container

Some files were not shown because too many files have changed in this diff Show More