实践自部署Overleaf

image.png

迫于大学对$LaTeX$排版软件的需求,自己帮忙为所在的大学部署了Overleaf的社区版本,并配置了完整的宏包支持,Shell Escape (用于代码高亮宏包minted)与自助注册功能。

以下配置步骤已在sharelatex:2.6.1上成功测试。

从3.1开始,Overleaf更换了下面所属的一些前端文件的目录,总的来说是把/var/www/sharelatex/里的东西全部迁移到了/overleaf/services/下面。因此如果使用是3.1及之后的overleaf镜像的话,需要注意替换一下需要修改的文件所在的目录。

安装

Overleaf为社区版本的安装提供了一个名为“Overleaf Toolkit”的工具,直接按照repo中的教程安装即可。

安装的过程中,相关脚本会创建一个名为sharelatex的container,根据Overleaf Wiki上的说明,目前安装的Overleaf中的TexLive版本仅为精简版,因此我们需要先安装上完整版的TexLive。

执行docker exec -it sharelatex bash进入容器,然后开始进行配置。

完整宏包支持(完整版TexLive)

参考 https://yxnchen.github.io/technique/Docker部署ShareLaTeX并简单配置中文环境/#安装并配置ShareLaTeX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 进入容器的命令行(sharelatex容器本质上是一个Ubuntu)
$ docker exec -it sharelatex bash

# 进入texlive默认安装目录
$ cd /usr/local/texlive

# 复制2020文件夹为2021
$ cp -a 2020 2021

# 下载并运行升级脚本
$ wget http://mirror.ctan.org/systems/texlive/tlnet/update-tlmgr-latest.sh
$ sh update-tlmgr-latest.sh -- --upgrade

# 更换texlive的下载源
$ tlmgr option repository https://mirrors.sustech.edu.cn/CTAN/systems/texlive/tlnet/

# 升级tlmgr
$ tlmgr update --self --all

# 安装完整版texlive(漫长的等待,不要让shell断开)
$ tlmgr install scheme-full

# 推出sharelatex的命令行界面,并重启sharelatex容器
$ exit
$ docker restart sharelatex

# 安装Noto字体(可选)
$ apt install fonts-noto-cjk

代码高亮(Minted包)支持

参考 https://harrychen.xyz/2020/02/15/self-host-overleaf-scientifically/

安装Python和pygments

1
2
3
#pygments 是用于代码高亮的包
$ apt install python3
$ apt-get install python-pygments

由于在安装完整版TexLive的时候已经安装了minted包了,现在就不需要另外安装了。

配置Shell Escape

修改/usr/local/texlive/2020/texmf.cnf,在最底下添加一行shell_escape = t

1
2
3
4
5
6
7
8
9
10
% (Public domain.)
% This texmf.cnf file should contain only your personal changes from the
% original texmf.cnf (for example, as chosen in the installer).
%
% That is, if you need to make changes to texmf.cnf, put your custom
% settings in this file, which is .../texlive/YYYY/texmf.cnf, rather than
% the distributed file (which is .../texlive/YYYY/texmf-dist/web2c/texmf.cnf).
% And include *only* your changed values, not a copy of the whole thing!
%
shell_escape = t

配置完成后重启容器。

配置自助注册

由于Overleaf对社区版的限制(可能是为了推销Server Pro),社区版的Overleaf默认不支持注册,进入注册页面之后只能看到一个please contact [email protected] to create an account.的提示。但是管理员是可以通过网页操作帮助用户创建账户的,因此我们可以用为管理员提供的用户注册界面魔改一个面向用户的注册界面。

修改router.js

Overleaf的后端是用Node.js写的,因此首先观察/var/www/sharelatex/web/app/src/router.jshttps://github.com/overleaf/web/blob/master/app/src/router.js#L929-L939),可以发现给管理员用的注册页面是通过POST /admin/register来实现注册的。因此我们在下面添加一个专门用于给用户注册的router,以及给这个router加上白名单(让未注册用户也能访问):

1
2
3
4
5
6
7
8
9
 webRouter.post(
'/self-register',
UserController.register_public
)
webRouter.get(
'/self-register',
UserPagesController.registerPage
)
AuthenticationController.addEndpointToLoginWhitelist('/self-register')

修改注册函数

之后我们可以找到UserController.register这个函数的位置/var/www/sharelatex/web/app/src/Features/User/UserController.js (https://github.com/overleaf/web/blob/master/app/src/Features/User/UserController.js#L459),在这个函数下面新增一个用于自助注册的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
register_public(req, res, next) {
const { email } = req.body
valid_edu = (/edu.cn\s*$/.test(email))
if (email == null || email === '' || !valid_edu) {
return res.sendStatus(422) // Unprocessable Entity
}
UserRegistrationHandler.registerNewUserAndSendActivationEmail(
email,
(error, user, setNewPasswordUrl) => {
if (error != null) {
return next(error)
}
setNewPasswordUrl = "Please check your inbox."
res.json({
email: user.email,
setNewPasswordUrl
})
}
)
},

由于我们可能只希望学校内部的人员可以注册,因此我们可以在邮箱格式判断上再加一条判断邮箱后缀或域名的规则(见上)。另外,在自助注册时,我们只希望用户从邮件中看到验证链接,而不能直接看到验证链接,因此我们需要把变量setNewPasswordUrl,设置成如“请检查收件箱”这样的提示语。

修改前端函数

我们还需要修改前端的函数,以便前端网页能够直接向我们在router.js里面写的注册url 发送数据。

修改/var/www/sharelatex/web/frontend/js/main/register-users.js (https://github.com/overleaf/web/blob/master/frontend/js/main/register-users.js#L32),在registerUsers()下面再新建一个registerUsers_pub()函数用于自助注册。

添加完之后的完整return如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  return ($scope.registerUsers = function() {
const emails = parseEmails($scope.inputs.emails)
$scope.error = false
return Array.from(emails).map(email =>
queuedHttp
.post('/admin/register', {
email,
_csrf: window.csrfToken
})
.then(function(response) {
const { data } = response
const user = data
$scope.users.push(user)
return ($scope.inputs.emails = '')
})
.catch(() => ($scope.error = true))
)
},

$scope.registerUsers_pub = function() {
const emails = parseEmails($scope.inputs.emails)
$scope.error = false
return Array.from(emails).map(email =>
queuedHttp
.post('/self-register', {
email,
_csrf: window.csrfToken
})
.then(function(response) {
const { data } = response
const user = data
$scope.users.push(user)
return ($scope.inputs.emails = '')
})
.catch(() => ($scope.error = true))
)
})

重新编译前端

/var/www/sharelatex/web/下运行npm run webpack:production

(如果webpack报错,很有可能是npm的版本太低,使用npm install -g npm更新npm,随后执行npm i再执行webpack命令即可)

随后修改/var/www/sharelatex/web/app/views/user/register.pug,将它改成和/var/www/sharelatex/web/app/views/admin/register.pug类似的结构(注意需修改注册使用的POST函数为刚刚新建的registerUsers_pub() ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
extends ../layout

block content
.content.content-alt
.container
.row
.col-md-12
.card(ng-controller="RegisterUsersController")
.page-header
h1 Register New Users
.row-spaced.ng-cloak
p This page only allows email ends with edu.cn to register.
p If you use other email address ends with other domain, please contact [email protected] .
form.form
.row
.col-md-4.col-xs-8
input.form-control(
name="email",
type="text",
placeholder="[email protected], [email protected]",
ng-model="inputs.emails",
on-enter="registerUsers_pub()"
)
.col-md-8.col-xs-4
button.btn.btn-primary(ng-click="registerUsers_pub()") #{translate("register")}

.row-spaced(ng-show="error").ng-cloak.text-danger
p Sorry, an error occured, check your email address or contact [email protected].

.row-spaced(ng-show="users.length > 0").ng-cloak.text-success
p We've sent out welcome emails to the registered users.
p This page only allows email ends with edu.cn to register.
p If you use other email address ends with other domain, please contact [email protected] .
p (Password reset tokens will expire after one week and the user will need registering again).

hr(ng-show="users.length > 0").ng-cloak
table(ng-show="users.length > 0").table.table-striped.ng-cloak
tr
th #{translate("email")}
th Set Password Url
tr(ng-repeat="user in users")
td {{ user.email }}
td(style="word-break: break-all;") {{ user.setNewPasswordUrl }}

最后退出shell,然后使用docker restart sharelatex重启container之后,上述功能就会被应用了。

image.png

添加Google Analytics(可选)

/var/www/sharelatex/web/app/views/layout.pug中插入脚本:

1
2
script(async='', src='https://www.googletagmanager.com/gtag/js?id=G-SSYEP475F4')
script(async='', src='/js/gtag.js')

将Google Analytics执行的脚本放在/var/www/sharelatex/web/public/js/gtag.js中:

1
2
3
4
5
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());

gtag('config', 'G-XXXXXXXX');

这样配置后,每次加载页面时,上述的Google Analytics脚本均会执行。


经过上面的魔改,一个比较科学的 Overleaf 服务就搭起来了。