Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35c5192b25 | |||
| e2905761c3 | |||
| 50ecae7357 | |||
| dceadd2ebe | |||
| 3cecfa3df4 | |||
| d9e6657879 | |||
| 151b786837 | |||
| 58b1ee5ebd | |||
| 1825c316a6 | |||
| d1dd3fa49c | |||
| eaeb4d1b96 | |||
| 688085c15e | |||
| 8d60c7d568 | |||
| be302f3025 | |||
| a3529d662f | |||
| a4f977e00b | |||
| b83482b4ba | |||
| 9ecaeda66e | |||
| 7fbcc58062 | |||
| 05f32114d1 | |||
| 5c79c2b431 | |||
| 3813b27862 | |||
| 042e9fcd81 | |||
| e8e43a7ee4 | |||
| a9d1e4311e | |||
| ed0fc0ec46 | |||
| fa307167f9 | |||
| 3f44844244 | |||
| 52925e9c7c | |||
| 188e515efc | |||
| cdd057c7a3 | |||
| 6d0d4640f6 | |||
| 6ca70c5bf2 | |||
| 95dfd945bc | |||
| 568ff1015b | |||
| 4b6ef9265b | |||
| b1ad8ccb73 | |||
| 758f84f33e | |||
| 3fcf865a4b | |||
| c1c11aaf60 | |||
| abc92df701 | |||
| 1dc8a66074 | |||
| bbe98a3254 | |||
| 5ca4c6d066 | |||
| 75e0bdcec5 | |||
| a918757105 | |||
| c07416b3d0 | |||
| 875579cc65 | |||
| 83cf348e07 | |||
| 7cb67cfd7f | |||
| 1c1c2d36e8 | |||
| 082600a50e | |||
| 5136c879c2 | |||
| ca414a7ccf | |||
| 331c32f9b6 | |||
| 298d05df3b | |||
| 85a8176708 | |||
| 0b5012c6fc | |||
| 0328f31fdc | |||
| 33fa93a952 | |||
| 68e405cf0b | |||
| b6280f4d21 | |||
| 1987c86f3c | |||
| c6176ee59f | |||
| e8c776c793 | |||
| cc64d4d2b2 | |||
| f0d55e4819 | |||
| fb14ca30eb | |||
| a672f066f7 | |||
| c6189cfcb9 | |||
| 42b2541cb5 | |||
| 9c93c6249c | |||
| b615a59db8 | |||
| e6bacf1fed | |||
| d6ae2b3c4e | |||
| 447b3e2475 | |||
| 7ecb1d63bb | |||
| b0981f6509 | |||
| 7f706bd171 |
@@ -21,3 +21,6 @@ exclude_dir = [
|
||||
]
|
||||
exclude_regex = ["_test.go$", "_gen.go$"]
|
||||
stop_on_error = true
|
||||
|
||||
[log]
|
||||
main_only = true
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!--
|
||||
1. Please speak English, this is the language all maintainers can speak and write.
|
||||
2. Please ask questions or configuration/deploy problems on our Discord
|
||||
server (https://discord.gg/gitea) or forum (https://discourse.gitea.io).
|
||||
server (https://discord.gg/gitea) or forum (https://forum.gitea.com).
|
||||
3. Please take a moment to check that your issue doesn't already exist.
|
||||
4. Make sure it's not mentioned in the FAQ (https://docs.gitea.com/help/faq)
|
||||
5. Please give all relevant information below for bug reports, because
|
||||
@@ -21,7 +21,7 @@
|
||||
- [ ] MySQL
|
||||
- [ ] MSSQL
|
||||
- [ ] SQLite
|
||||
- Can you reproduce the bug at https://try.gitea.io:
|
||||
- Can you reproduce the bug at https://demo.gitea.com:
|
||||
- [ ] Yes (provide example URL)
|
||||
- [ ] No
|
||||
- Log gist:
|
||||
|
||||
@@ -37,7 +37,7 @@ body:
|
||||
label: Can you reproduce the bug on the Gitea demo site?
|
||||
description: |
|
||||
If so, please provide a URL in the Description field
|
||||
URL of Gitea demo: https://try.gitea.io
|
||||
URL of Gitea demo: https://demo.gitea.com
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
@@ -74,7 +74,7 @@ body:
|
||||
attributes:
|
||||
label: How are you running Gitea?
|
||||
description: |
|
||||
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://try.gitea.io or are using some other package
|
||||
Please include information on whether you built Gitea yourself, used one of our downloads, are using https://demo.gitea.com or are using some other package
|
||||
Please also tell us how you are running Gitea, e.g. if it is being run from docker, a command-line, systemd etc.
|
||||
If you are using a package or systemd tell us what distribution you are using
|
||||
validations:
|
||||
|
||||
@@ -46,7 +46,7 @@ body:
|
||||
label: Can you reproduce the bug on the Gitea demo site?
|
||||
description: |
|
||||
If so, please provide a URL in the Description field
|
||||
URL of Gitea demo: https://try.gitea.io
|
||||
URL of Gitea demo: https://demo.gitea.com
|
||||
options:
|
||||
- "Yes"
|
||||
- "No"
|
||||
|
||||
@@ -4,6 +4,79 @@ This changelog goes through the changes that have been made in each release
|
||||
without substantial changes to our git log; to see the highlights of what has
|
||||
been added to each release, please refer to the [blog](https://blog.gitea.com).
|
||||
|
||||
## [1.22.1](https://github.com/go-gitea/gitea/releases/tag/1.22.1) - 2024-07-04
|
||||
|
||||
* SECURITY
|
||||
* Add replacement module for `mholt/archiver` (#31267) (#31270)
|
||||
* API
|
||||
* Fix missing images in editor preview due to wrong links (#31299) (#31393)
|
||||
* Fix duplicate sub-path for avatars (#31365) (#31368)
|
||||
* Reduce memory usage for chunked artifact uploads to MinIO (#31325) (#31338)
|
||||
* Remove sub-path from container registry realm (#31293) (#31300)
|
||||
* Fix NuGet Package API for $filter with Id equality (#31188) (#31242)
|
||||
* Add an immutable tarball link to archive download headers for Nix (#31139) (#31145)
|
||||
* Add missed return after `ctx.ServerError` (#31130) (#31133)
|
||||
* BUGFIXES
|
||||
* Fix avatar radius problem on the new issue page (#31506) (#31508)
|
||||
* Fix overflow menu flickering on mobile (#31484) (#31488)
|
||||
* Fix poor table column width due to breaking words (#31473) (#31477)
|
||||
* Support relative paths to videos from Wiki pages (#31061) (#31453)
|
||||
* Fix new issue/pr avatar (#31419) (#31424)
|
||||
* Increase max length of org team names from 30 to 255 characters (#31410) (#31421)
|
||||
* Fix line number width in code preview (#31307) (#31316)
|
||||
* Optimize runner-tags layout to enhance visual experience (#31258) (#31263)
|
||||
* Fix overflow on push notification (#31179) (#31238)
|
||||
* Fix overflow on notifications (#31178) (#31237)
|
||||
* Fix overflow in issue card (#31203) (#31225)
|
||||
* Split sanitizer functions and fine-tune some tests (#31192) (#31200)
|
||||
* use correct l10n string (#31487) (#31490)
|
||||
* Fix dropzone JS error when attachment is disabled (#31486)
|
||||
* Fix web notification icon not updated once you read all notifications (#31447) (#31466)
|
||||
* Switch to "Write" tab when edit comment again (#31445) (#31461)
|
||||
* Fix the link for .git-blame-ignore-revs bypass (#31432) (#31442)
|
||||
* Fix the wrong line number in the diff view page when expanded twice. (#31431) (#31440)
|
||||
* Fix labels and projects menu overflow on issue page (#31435) (#31439)
|
||||
* Fix Account Linking UpdateMigrationsByType (#31428) (#31434)
|
||||
* Fix markdown math brackets render problem (#31420) (#31430)
|
||||
* Fix rendered wiki page link (#31398) (#31407)
|
||||
* Fix natural sort (#31384) (#31394)
|
||||
* Allow downloading attachments of draft releases (#31369) (#31380)
|
||||
* Fix repo graph JS (#31377)
|
||||
* Fix incorrect localization `explorer.go` (#31348) (#31350)
|
||||
* Fix hash render end with colon (#31319) (#31346)
|
||||
* Fix line number widths (#31341) (#31343)
|
||||
* Fix navbar `+` menu flashing on page load (#31281) (#31342)
|
||||
* Fix adopt repository has empty object name in database (#31333) (#31335)
|
||||
* Delete legacy cookie before setting new cookie (#31306) (#31317)
|
||||
* Fix some URLs whose sub-path is missing (#31289) (#31292)
|
||||
* Fix admin oauth2 custom URL settings (#31246) (#31247)
|
||||
* Make pasted "img" tag has the same behavior as markdown image (#31235) (#31243)
|
||||
* Fix agit checkout command line hint & fix ShowMergeInstructions checking (#31219) (#31222)
|
||||
* Fix the possible migration failure on 286 with postgres 16 (#31209) (#31218)
|
||||
* Fix branch order (#31174) (#31193)
|
||||
* Fix markup preview (#31158) (#31166)
|
||||
* Fix push multiple branches error with tests (#31151) (#31153)
|
||||
* Fix API repository object format missed (#31118) (#31132)
|
||||
* Fix missing memcache import (#31105) (#31109)
|
||||
* Upgrade `github.com/hashicorp/go-retryablehttp` (#31499)
|
||||
* Fix double border in system status table (#31363) (#31401)
|
||||
* Fix bug filtering issues which have no project (#31337) (#31367)
|
||||
* Fix #31185 try fix lfs download from bitbucket failed (#31201) (#31329)
|
||||
* Add nix flake for dev shell (#30967) (#31310)
|
||||
* Fix and clean up `ConfirmModal` (#31283) (#31291)
|
||||
* Optimize repo-list layout to enhance visual experience (#31272) (#31276)
|
||||
* fixed the dropdown menu for the top New button to expand to the left (#31273) (#31275)
|
||||
* Fix Activity Page Contributors dropdown (#31264) (#31269)
|
||||
* fix: allow actions artifacts storage migration to complete succesfully (#31251) (#31257)
|
||||
* Make blockquote attention recognize more syntaxes (#31240) (#31250)
|
||||
* Remove .segment from .project-column (#31204) (#31239)
|
||||
* Ignore FindRecentlyPushedNewBranches err (#31164) (#31171)
|
||||
* Use vertical layout for multiple code expander buttons (#31122) (#31152)
|
||||
* Remove duplicate `ProxyPreserveHost` in Apache httpd doc (#31143) (#31147)
|
||||
* Improve mobile review ui (#31091) (#31136)
|
||||
* Fix DashboardRepoList margin (#31121) (#31128)
|
||||
* Update pip related commands for docker (#31106) (#31111)
|
||||
|
||||
## [1.22.0](https://github.com/go-gitea/gitea/releases/tag/v1.22.0) - 2024-05-27
|
||||
|
||||
This release stands as a monumental milestone in our development journey with a record-breaking incorporation of [1528](https://github.com/go-gitea/gitea/pulls?q=is%3Apr+milestone%3A1.22.0+is%3Amerged) pull requests. It marks the most extensive update in Gitea's history, showcasing a plethora of new features and infrastructure improvements.
|
||||
|
||||
+2
-2
@@ -77,7 +77,7 @@ If your issue has not been reported yet, [open an issue](https://github.com/go-g
|
||||
and answer the questions so we can understand and reproduce the problematic behavior. \
|
||||
Please write clear and concise instructions so that we can reproduce the behavior — even if it seems obvious. \
|
||||
The more detailed and specific you are, the faster we can fix the issue. \
|
||||
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://try.gitea.io>, as perhaps your problem has already been fixed on a current version. \
|
||||
It is really helpful if you can reproduce your problem on a site running on the latest commits, i.e. <https://demo.gitea.com>, as perhaps your problem has already been fixed on a current version. \
|
||||
Please follow the guidelines described in [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) for your report.
|
||||
|
||||
Please be kind, remember that Gitea comes at no cost to you, and you're getting free help.
|
||||
@@ -362,7 +362,7 @@ If you add a new feature or change an existing aspect of Gitea, the documentatio
|
||||
|
||||
## API v1
|
||||
|
||||
The API is documented by [swagger](http://try.gitea.io/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
|
||||
The API is documented by [swagger](https://gitea.com/api/swagger) and is based on [the GitHub API](https://docs.github.com/en/rest).
|
||||
|
||||
### GitHub API compatibility
|
||||
|
||||
|
||||
+4
-4
@@ -2,11 +2,11 @@
|
||||
FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
ENV TAGS="bindata timetzdata $TAGS"
|
||||
ARG CGO_EXTRA_CFLAGS
|
||||
|
||||
# Build deps
|
||||
@@ -72,8 +72,8 @@ RUN addgroup \
|
||||
git && \
|
||||
echo "git:*" | chpasswd -e
|
||||
|
||||
ENV USER git
|
||||
ENV GITEA_CUSTOM /data/gitea
|
||||
ENV USER=git
|
||||
ENV GITEA_CUSTOM=/data/gitea
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
|
||||
+8
-8
@@ -2,11 +2,11 @@
|
||||
FROM docker.io/library/golang:1.22-alpine3.20 AS build-env
|
||||
|
||||
ARG GOPROXY
|
||||
ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=${GOPROXY:-direct}
|
||||
|
||||
ARG GITEA_VERSION
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
ENV TAGS="bindata timetzdata $TAGS"
|
||||
ARG CGO_EXTRA_CFLAGS
|
||||
|
||||
#Build deps
|
||||
@@ -75,14 +75,14 @@ COPY --from=build-env /go/src/code.gitea.io/gitea/contrib/autocompletion/bash_au
|
||||
|
||||
# git:git
|
||||
USER 1000:1000
|
||||
ENV GITEA_WORK_DIR /var/lib/gitea
|
||||
ENV GITEA_CUSTOM /var/lib/gitea/custom
|
||||
ENV GITEA_TEMP /tmp/gitea
|
||||
ENV TMPDIR /tmp/gitea
|
||||
ENV GITEA_WORK_DIR=/var/lib/gitea
|
||||
ENV GITEA_CUSTOM=/var/lib/gitea/custom
|
||||
ENV GITEA_TEMP=/tmp/gitea
|
||||
ENV TMPDIR=/tmp/gitea
|
||||
|
||||
# TODO add to docs the ability to define the ini to load (useful to test and revert a config)
|
||||
ENV GITEA_APP_INI /etc/gitea/app.ini
|
||||
ENV HOME "/var/lib/gitea/git"
|
||||
ENV GITEA_APP_INI=/etc/gitea/app.ini
|
||||
ENV HOME="/var/lib/gitea/git"
|
||||
VOLUME ["/var/lib/gitea", "/etc/gitea"]
|
||||
WORKDIR /var/lib/gitea
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ COMMA := ,
|
||||
|
||||
XGO_VERSION := go-1.22.x
|
||||
|
||||
AIR_PACKAGE ?= github.com/cosmtrek/air@v1
|
||||
AIR_PACKAGE ?= github.com/air-verse/air@v1
|
||||
EDITORCONFIG_CHECKER_PACKAGE ?= github.com/editorconfig-checker/editorconfig-checker/cmd/editorconfig-checker@2.7.0
|
||||
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.6.0
|
||||
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2
|
||||
|
||||
@@ -26,7 +26,7 @@ This project has been
|
||||
[forked](https://blog.gitea.com/welcome-to-gitea/) from
|
||||
[Gogs](https://gogs.io) since November of 2016, but a lot has changed.
|
||||
|
||||
For online demonstrations, you can visit [try.gitea.io](https://try.gitea.io).
|
||||
For online demonstrations, you can visit [demo.gitea.com](https://demo.gitea.com).
|
||||
|
||||
For accessing free Gitea service (with a limited number of repositories), you can visit [gitea.com](https://gitea.com/user/login).
|
||||
|
||||
@@ -56,7 +56,7 @@ More info: https://docs.gitea.com/installation/install-from-source
|
||||
./gitea web
|
||||
|
||||
> [!NOTE]
|
||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://try.gitea.io/api/swagger).
|
||||
> If you're interested in using our APIs, we have experimental support with [documentation](https://docs.gitea.com/api).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -80,7 +80,7 @@ https://docs.gitea.com/contributing/localization
|
||||
## Further information
|
||||
|
||||
For more information and instructions about how to install Gitea, please look at our [documentation](https://docs.gitea.com/).
|
||||
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://discourse.gitea.io/).
|
||||
If you have questions that are not covered by the documentation, you can get in contact with us on our [Discord server](https://discord.gg/Gitea) or create a post in the [discourse forum](https://forum.gitea.com/).
|
||||
|
||||
We maintain a list of Gitea-related projects at [gitea/awesome-gitea](https://gitea.com/gitea/awesome-gitea).
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
|
||||
Gitea 的首要目标是创建一个极易安装,运行非常快速,安装和使用体验良好的自建 Git 服务。我们采用 Go 作为后端语言,这使我们只要生成一个可执行程序即可。并且他还支持跨平台,支持 Linux, macOS 和 Windows 以及各种架构,除了 x86,amd64,还包括 ARM 和 PowerPC。
|
||||
|
||||
如果你想试用在线演示,请访问 [try.gitea.io](https://try.gitea.io/)。
|
||||
如果你想试用在线演示和报告问题,请访问 [demo.gitea.com](https://demo.gitea.com/)。
|
||||
|
||||
如果你想使用免费的 Gitea 服务(有仓库数量限制),请访问 [gitea.com](https://gitea.com/user/login)。
|
||||
|
||||
|
||||
+16
-2
@@ -5,7 +5,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
@@ -162,8 +164,20 @@ func migrateActionsLog(ctx context.Context, dstStorage storage.ObjectStorage) er
|
||||
|
||||
func migrateActionsArtifacts(ctx context.Context, dstStorage storage.ObjectStorage) error {
|
||||
return db.Iterate(ctx, nil, func(ctx context.Context, artifact *actions_model.ActionArtifact) error {
|
||||
_, err := storage.Copy(dstStorage, artifact.ArtifactPath, storage.ActionsArtifacts, artifact.ArtifactPath)
|
||||
return err
|
||||
if artifact.Status == int64(actions_model.ArtifactStatusExpired) {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := storage.Copy(dstStorage, artifact.StoragePath, storage.ActionsArtifacts, artifact.StoragePath)
|
||||
if err != nil {
|
||||
// ignore files that do not exist
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
# Gitea: Docs
|
||||
|
||||
[](https://discord.gg/Gitea)
|
||||
[](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
|
||||
|
||||
These docs are ingested by our [docs repo](https://gitea.com/gitea/gitea-docusaurus).
|
||||
|
||||
## Authors
|
||||
@@ -18,5 +15,5 @@ for the full license text.
|
||||
## Copyright
|
||||
|
||||
```
|
||||
Copyright (c) 2016 The Gitea Authors <https://gitea.io>
|
||||
Copyright (c) 2016 The Gitea Authors
|
||||
```
|
||||
|
||||
+1
-5
@@ -1,9 +1,5 @@
|
||||
# Gitea: 文档
|
||||
|
||||
[](http://drone.gitea.io/go-gitea/docs)
|
||||
[](https://discord.gg/Gitea)
|
||||
[](http://microbadger.com/images/gitea/docs "Get your own image badge on microbadger.com")
|
||||
|
||||
https://gitea.com/gitea/gitea-docusaurus
|
||||
|
||||
## 关于我们
|
||||
@@ -18,5 +14,5 @@ https://gitea.com/gitea/gitea-docusaurus
|
||||
## 版权声明
|
||||
|
||||
```
|
||||
Copyright (c) 2016 The Gitea Authors <https://gitea.io>
|
||||
Copyright (c) 2016 The Gitea Authors
|
||||
```
|
||||
|
||||
@@ -38,12 +38,10 @@ FROM gitea/gitea:@version@
|
||||
COPY custom/app.ini /data/gitea/conf/app.ini
|
||||
[...]
|
||||
|
||||
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
|
||||
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
|
||||
# install any other package you need for your external renderers
|
||||
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN pip3 install -U setuptools
|
||||
RUN pip3 install jupyter docutils
|
||||
RUN pipx install jupyter docutils --include-deps
|
||||
# add above any other python package you may need to install
|
||||
```
|
||||
|
||||
|
||||
@@ -37,12 +37,10 @@ FROM gitea/gitea:@version@
|
||||
COPY custom/app.ini /data/gitea/conf/app.ini
|
||||
[...]
|
||||
|
||||
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev py-pip python3-dev py3-pip py3-pyzmq
|
||||
RUN apk --no-cache add asciidoctor freetype freetype-dev gcc g++ libpng libffi-dev pandoc python3-dev py3-pyzmq pipx
|
||||
# 安装其他您需要的外部渲染器的软件包
|
||||
|
||||
RUN pip3 install --upgrade pip
|
||||
RUN pip3 install -U setuptools
|
||||
RUN pip3 install jupyter docutils
|
||||
RUN pipx install jupyter docutils --include-deps
|
||||
# 在上面添加您需要安装的任何其他 Python 软件包
|
||||
```
|
||||
|
||||
|
||||
@@ -169,7 +169,6 @@ If you want Apache HTTPD to serve your Gitea instance, you can add the following
|
||||
ProxyRequests off
|
||||
AllowEncodedSlashes NoDecode
|
||||
ProxyPass / http://localhost:3000/ nocanon
|
||||
ProxyPreserveHost On
|
||||
RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
@@ -117,7 +117,7 @@ curl -v "http://localhost/api/v1/repos/search?limit=1"
|
||||
API Reference guide is auto-generated by swagger and available on:
|
||||
`https://gitea.your.host/api/swagger`
|
||||
or on the
|
||||
[Gitea demo instance](https://try.gitea.io/api/swagger)
|
||||
[Gitea instance](https://gitea.com/api/swagger)
|
||||
|
||||
The OpenAPI document is at:
|
||||
`https://gitea.your.host/swagger.v1.json`
|
||||
|
||||
@@ -45,7 +45,7 @@ To migrate from GitHub to Gitea, you can use Gitea's built-in migration form.
|
||||
|
||||
In order to migrate items such as issues, pull requests, etc. you will need to input at least your username.
|
||||
|
||||
[Example (requires login)](https://try.gitea.io/repo/migrate)
|
||||
[Example (requires login)](https://demo.gitea.com/repo/migrate)
|
||||
|
||||
To migrate from GitLab to Gitea, you can use this non-affiliated tool:
|
||||
|
||||
@@ -137,9 +137,9 @@ All Gitea instances have the built-in API and there is no way to disable it comp
|
||||
You can, however, disable showing its documentation by setting `ENABLE_SWAGGER` to `false` in the `api` section of your `app.ini`.
|
||||
For more information, refer to Gitea's [API docs](development/api-usage.md).
|
||||
|
||||
You can see the latest API (for example) on https://try.gitea.io/api/swagger
|
||||
You can see the latest API (for example) on https://gitea.com/api/swagger
|
||||
|
||||
You can also see an example of the `swagger.json` file at https://try.gitea.io/swagger.v1.json
|
||||
You can also see an example of the `swagger.json` file at https://gitea.com/swagger.v1.json
|
||||
|
||||
## Adjusting your server for public/private use
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ menu:
|
||||
|
||||
为了迁移诸如问题、拉取请求等项目,您需要至少输入您的用户名。
|
||||
|
||||
[Example (requires login)](https://try.gitea.io/repo/migrate)
|
||||
[Example (requires login)](https://demo.gitea.com/repo/migrate)
|
||||
|
||||
要从GitLab迁移到Gitea,您可以使用这个非关联的工具:
|
||||
|
||||
@@ -141,9 +141,9 @@ Gitea不提供内置的Pages服务器。您需要一个专用的域名来提供
|
||||
但是,您可以在app.ini的api部分将ENABLE_SWAGGER设置为false,以禁用其文档显示。
|
||||
有关更多信息,请参阅Gitea的[API文档](development/api-usage.md)。
|
||||
|
||||
您可以在上查看最新的API(例如)https://try.gitea.io/api/swagger
|
||||
您可以在上查看最新的API(例如)https://gitea.com/api/swagger
|
||||
|
||||
您还可以在上查看`swagger.json`文件的示例 https://try.gitea.io/swagger.v1.json
|
||||
您还可以在上查看`swagger.json`文件的示例 https://gitea.com/swagger.v1.json
|
||||
|
||||
## 调整服务器用于公共/私有使用
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ menu:
|
||||
|
||||
- [Paid Commercial Support](https://about.gitea.com/)
|
||||
- [Discord](https://discord.gg/Gitea)
|
||||
- [Discourse Forum](https://discourse.gitea.io/)
|
||||
- [Forum](https://forum.gitea.com/)
|
||||
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
|
||||
- NOTE: Most of the Matrix channels are bridged with their counterpart in Discord and may experience some degree of flakiness with the bridge process.
|
||||
- Chinese Support
|
||||
- [Discourse Chinese Category](https://discourse.gitea.io/c/5-category/5)
|
||||
- [Discourse Chinese Category](https://forum.gitea.com/c/5-category/5)
|
||||
- QQ Group 328432459
|
||||
|
||||
# Bug Report
|
||||
@@ -39,7 +39,7 @@ If you found a bug, please [create an issue on GitHub](https://github.com/go-git
|
||||
- When using systemd, use `journalctl --lines 1000 --unit gitea` to collect logs.
|
||||
- When using docker, use `docker logs --tail 1000 <gitea-container>` to collect logs.
|
||||
4. Reproducible steps so that others could reproduce and understand the problem more quickly and easily.
|
||||
- [try.gitea.io](https://try.gitea.io) could be used to reproduce the problem.
|
||||
- [demo.gitea.com](https://demo.gitea.com) could be used to reproduce the problem.
|
||||
5. If you encounter slow/hanging/deadlock problems, please report the stacktrace when the problem occurs.
|
||||
Go to the "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report".
|
||||
|
||||
|
||||
@@ -19,11 +19,11 @@ menu:
|
||||
|
||||
- [付费商业支持](https://about.gitea.com/)
|
||||
- [Discord](https://discord.gg/Gitea)
|
||||
- [Discourse 论坛](https://discourse.gitea.io/)
|
||||
- [论坛](https://forum.gitea.com/)
|
||||
- [Matrix](https://matrix.to/#/#gitea-space:matrix.org)
|
||||
- 注意:大多数 Matrix 频道都与 Discord 中的对应频道桥接,可能在桥接过程中会出现一定程度的不稳定性。
|
||||
- 中文支持
|
||||
- [Discourse 中文分类](https://discourse.gitea.io/c/5-category/5)
|
||||
- [Discourse 中文分类](https://forum.gitea.com/c/5-category/5)
|
||||
- QQ 群 328432459
|
||||
|
||||
# Bug 报告
|
||||
@@ -39,7 +39,7 @@ menu:
|
||||
- 在使用 systemd 时,使用 `journalctl --lines 1000 --unit gitea` 收集日志。
|
||||
- 在使用 Docker 时,使用 `docker logs --tail 1000 <gitea-container>` 收集日志。
|
||||
4. 可重现的步骤,以便他人能够更快速、更容易地重现和理解问题。
|
||||
- [try.gitea.io](https://try.gitea.io) 可用于重现问题。
|
||||
- [demo.gitea.com](https://demo.gitea.com) 可用于重现问题。
|
||||
5. 如果遇到慢速/挂起/死锁等问题,请在出现问题时报告堆栈跟踪。
|
||||
转到 "Site Admin" -> "Monitoring" -> "Stacktrace" -> "Download diagnosis report"。
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ up a self-hosted Git service.
|
||||
With Go, this can be done platform-independently across
|
||||
**all platforms** which Go supports, including Linux, macOS, and Windows,
|
||||
on x86, amd64, ARM and PowerPC architectures.
|
||||
You can try it out using [the online demo](https://try.gitea.io/).
|
||||
You can try it out using [the online demo](https://demo.gitea.com).
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -5,11 +5,9 @@ slug: "badge"
|
||||
sidebar_position: 11
|
||||
toc: false
|
||||
draft: false
|
||||
aliases:
|
||||
- /en-us/badge
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
parent: "actions"
|
||||
name: "Badge"
|
||||
sidebar_position: 11
|
||||
identifier: "Badge"
|
||||
@@ -27,7 +25,7 @@ It is designed to be compatible with [GitHub Actions workflow badge](https://doc
|
||||
You can use the following URL to get the badge:
|
||||
|
||||
```
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}?branch={branch}&event={event}
|
||||
https://your-gitea-instance.com/{owner}/{repo}/actions/workflows/{workflow_file}/badge.svg?branch={branch}&event={event}
|
||||
```
|
||||
|
||||
- `{owner}`: The owner of the repository.
|
||||
@@ -5,11 +5,9 @@ slug: "secrets"
|
||||
sidebar_position: 50
|
||||
draft: false
|
||||
toc: false
|
||||
aliases:
|
||||
- /en-us/secrets
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
parent: "actions"
|
||||
name: "Secrets"
|
||||
sidebar_position: 50
|
||||
identifier: "usage-secrets"
|
||||
@@ -5,11 +5,9 @@ slug: "secrets"
|
||||
sidebar_position: 50
|
||||
draft: false
|
||||
toc: false
|
||||
aliases:
|
||||
- /zh-cn/secrets
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
parent: "actions"
|
||||
name: "密钥管理"
|
||||
sidebar_position: 50
|
||||
identifier: "usage-secrets"
|
||||
@@ -236,7 +236,7 @@ configure this, set the fields below:
|
||||
|
||||
- Restrict what domains can log in if using a public SMTP host or SMTP host
|
||||
with multiple domains.
|
||||
- Example: `gitea.io,mydomain.com,mydomain2.com`
|
||||
- Example: `gitea.com,mydomain.com,mydomain2.com`
|
||||
|
||||
- Force SMTPS
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ PAM提供了一种机制,通过对用户进行PAM认证来自动将其添加
|
||||
|
||||
- 如果使用公共 SMTP 主机或有多个域的 SMTP 主机,限制哪些域可以登录
|
||||
限制哪些域可以登录。
|
||||
- 示例: `gitea.io,mydomain.com,mydomain2.com`
|
||||
- 示例: `gitea.com,mydomain.com,mydomain2.com`
|
||||
|
||||
- 强制使用 SMTPS
|
||||
- 默认情况下将使用SMTPS连接到端口465.如果您希望将smtp用于其他端口,自行设置
|
||||
|
||||
@@ -308,7 +308,7 @@ This is a example for a issue config file
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Gitea
|
||||
url: https://gitea.io
|
||||
url: https://gitea.com
|
||||
about: Visit the Gitea Website
|
||||
```
|
||||
|
||||
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1715534503,
|
||||
"narHash": "sha256-5ZSVkFadZbFP1THataCaSf0JH2cAH3S29hU9rrxTEqk=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "2057814051972fa1453ddfb0d98badbea9b83c06",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
outputs =
|
||||
{ nixpkgs, flake-utils, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# generic
|
||||
git
|
||||
git-lfs
|
||||
gnumake
|
||||
gnused
|
||||
gnutar
|
||||
gzip
|
||||
|
||||
# frontend
|
||||
nodejs_20
|
||||
|
||||
# linting
|
||||
python312
|
||||
poetry
|
||||
|
||||
# backend
|
||||
go_1_22
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -56,7 +56,7 @@ require (
|
||||
github.com/google/go-github/v57 v57.0.0
|
||||
github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.1.2
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/gorilla/sessions v1.2.2
|
||||
github.com/hashicorp/go-version v1.6.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
@@ -104,13 +104,13 @@ require (
|
||||
github.com/yuin/goldmark v1.7.0
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
github.com/yuin/goldmark-meta v1.1.0
|
||||
golang.org/x/crypto v0.22.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.24.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/oauth2 v0.18.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/text v0.14.0
|
||||
golang.org/x/tools v0.19.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
|
||||
google.golang.org/grpc v1.62.1
|
||||
google.golang.org/protobuf v1.33.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
@@ -210,7 +210,7 @@ require (
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
@@ -288,8 +288,8 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
|
||||
@@ -304,7 +304,8 @@ replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142
|
||||
|
||||
replace github.com/nektos/act => gitea.com/gitea/act v0.259.1
|
||||
|
||||
replace github.com/gorilla/feeds => github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5
|
||||
// TODO: This could be removed after https://github.com/mholt/archiver/pull/396 merged
|
||||
replace github.com/mholt/archiver/v3 => github.com/anchore/archiver/v3 v3.5.2
|
||||
|
||||
exclude github.com/gofrs/uuid v3.2.0+incompatible
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/anchore/archiver/v3 v3.5.2 h1:Bjemm2NzuRhmHy3m0lRe5tNoClB9A4zYyDV58PaB6aA=
|
||||
github.com/anchore/archiver/v3 v3.5.2/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
@@ -418,6 +420,8 @@ github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc=
|
||||
github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
|
||||
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
@@ -432,11 +436,10 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj
|
||||
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c=
|
||||
github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
@@ -554,8 +557,6 @@ github.com/meilisearch/meilisearch-go v0.26.2 h1:3gTlmiV1dHHumVUhYdJbvh3camiNiyq
|
||||
github.com/meilisearch/meilisearch-go v0.26.2/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
|
||||
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
|
||||
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
|
||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
|
||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/microsoft/go-mssqldb v1.7.0 h1:sgMPW0HA6Ihd37Yx0MzHyKD726C2kY/8KJsQtXHNaAs=
|
||||
@@ -799,8 +800,6 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5 h1:3seWKGVhGoc66Ht5QlhQsr4xT2caDnFegsnh2NqvENU=
|
||||
github.com/yardenshoham/feeds v0.0.0-20240110072658-f3d0c21c0bd5/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
|
||||
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
|
||||
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
@@ -854,20 +853,20 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -888,8 +887,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -900,8 +899,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -939,8 +938,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
@@ -950,8 +949,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -963,8 +962,9 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -979,8 +979,8 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
|
||||
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -286,13 +286,14 @@ type UserIDCount struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
// GetUIDsAndNotificationCounts between the two provided times
|
||||
// GetUIDsAndNotificationCounts returns the unread counts for every user between the two provided times.
|
||||
// It must return all user IDs which appear during the period, including count=0 for users who have read all.
|
||||
func GetUIDsAndNotificationCounts(ctx context.Context, since, until timeutil.TimeStamp) ([]UserIDCount, error) {
|
||||
sql := `SELECT user_id, count(*) AS count FROM notification ` +
|
||||
sql := `SELECT user_id, sum(case when status= ? then 1 else 0 end) AS count FROM notification ` +
|
||||
`WHERE user_id IN (SELECT user_id FROM notification WHERE updated_unix >= ? AND ` +
|
||||
`updated_unix < ?) AND status = ? GROUP BY user_id`
|
||||
`updated_unix < ?) GROUP BY user_id`
|
||||
var res []UserIDCount
|
||||
return res, db.GetEngine(ctx).SQL(sql, since, until, NotificationStatusUnread).Find(&res)
|
||||
return res, db.GetEngine(ctx).SQL(sql, NotificationStatusUnread, since, until).Find(&res)
|
||||
}
|
||||
|
||||
// SetIssueReadBy sets issue to be read by given user.
|
||||
|
||||
@@ -107,17 +107,13 @@ func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||
|
||||
func (opts FindBranchOptions) ToOrders() string {
|
||||
orderBy := opts.OrderBy
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the end
|
||||
if orderBy != "" {
|
||||
orderBy += ", "
|
||||
}
|
||||
orderBy += "is_deleted ASC"
|
||||
}
|
||||
if orderBy == "" {
|
||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
||||
return "commit_time DESC, name ASC"
|
||||
orderBy = "commit_time DESC, name ASC"
|
||||
}
|
||||
if opts.IsDeletedBranch.ValueOrDefault(true) { // if deleted branch included, put them at the beginning
|
||||
orderBy = "is_deleted ASC, " + orderBy
|
||||
}
|
||||
|
||||
return orderBy
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ func addObjectFormatNameToRepository(x *xorm.Engine) error {
|
||||
|
||||
// Here to catch weird edge-cases where column constraints above are
|
||||
// not applied by the DB backend
|
||||
_, err := x.Exec("UPDATE repository set object_format_name = 'sha1' WHERE object_format_name = '' or object_format_name IS NULL")
|
||||
_, err := x.Exec("UPDATE `repository` set `object_format_name` = 'sha1' WHERE `object_format_name` = '' or `object_format_name` IS NULL")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepoAvatarLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||
|
||||
repo := &Repository{ID: 1, Avatar: "avatar.png"}
|
||||
link := repo.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/repo-avatars/avatar.png", link)
|
||||
|
||||
setting.AppURL = "https://localhost/sub-path/"
|
||||
setting.AppSubURL = "/sub-path"
|
||||
link = repo.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/repo-avatars/avatar.png", link)
|
||||
}
|
||||
@@ -89,9 +89,11 @@ func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
|
||||
}
|
||||
|
||||
// AvatarLink returns the full avatar url with http host. TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
// AvatarLink returns the full avatar url with http host.
|
||||
// TODO: refactor it to a relative URL, but it is still used in API response at the moment
|
||||
func (u *User) AvatarLink(ctx context.Context) string {
|
||||
return httplib.MakeAbsoluteURL(ctx, u.AvatarLinkWithSize(ctx, 0))
|
||||
relLink := u.AvatarLinkWithSize(ctx, 0) // it can't be empty
|
||||
return httplib.MakeAbsoluteURL(ctx, relLink)
|
||||
}
|
||||
|
||||
// IsUploadAvatarChanged returns true if the current user's avatar would be changed with the provided data
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserAvatarLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, "https://localhost/")()
|
||||
defer test.MockVariableValue(&setting.AppSubURL, "")()
|
||||
|
||||
u := &User{ID: 1, Avatar: "avatar.png"}
|
||||
link := u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/avatars/avatar.png", link)
|
||||
|
||||
setting.AppURL = "https://localhost/sub-path/"
|
||||
setting.AppSubURL = "/sub-path"
|
||||
link = u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
|
||||
}
|
||||
@@ -4,12 +4,67 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func naturalSortGetRune(str string, pos int) (r rune, size int, has bool) {
|
||||
if pos >= len(str) {
|
||||
return 0, 0, false
|
||||
}
|
||||
r, size = utf8.DecodeRuneInString(str[pos:])
|
||||
if r == utf8.RuneError {
|
||||
r, size = rune(str[pos]), 1 // if invalid input, treat it as a single byte ascii
|
||||
}
|
||||
return r, size, true
|
||||
}
|
||||
|
||||
func naturalSortAdvance(str string, pos int) (end int, isNumber bool) {
|
||||
end = pos
|
||||
for {
|
||||
r, size, has := naturalSortGetRune(str, end)
|
||||
if !has {
|
||||
break
|
||||
}
|
||||
isCurRuneNum := '0' <= r && r <= '9'
|
||||
if end == pos {
|
||||
isNumber = isCurRuneNum
|
||||
end += size
|
||||
} else if isCurRuneNum == isNumber {
|
||||
end += size
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return end, isNumber
|
||||
}
|
||||
|
||||
// NaturalSortLess compares two strings so that they could be sorted in natural order
|
||||
func NaturalSortLess(s1, s2 string) bool {
|
||||
// There is a bug in Golang's collate package: https://github.com/golang/go/issues/67997
|
||||
// text/collate: CompareString(collate.Numeric) returns wrong result for "0.0" vs "1.0" #67997
|
||||
// So we need to handle the number parts by ourselves
|
||||
c := collate.New(language.English, collate.Numeric)
|
||||
return c.CompareString(s1, s2) < 0
|
||||
pos1, pos2 := 0, 0
|
||||
for pos1 < len(s1) && pos2 < len(s2) {
|
||||
end1, isNum1 := naturalSortAdvance(s1, pos1)
|
||||
end2, isNum2 := naturalSortAdvance(s2, pos2)
|
||||
part1, part2 := s1[pos1:end1], s2[pos2:end2]
|
||||
if isNum1 && isNum2 {
|
||||
if part1 != part2 {
|
||||
if len(part1) != len(part2) {
|
||||
return len(part1) < len(part2)
|
||||
}
|
||||
return part1 < part2
|
||||
}
|
||||
} else {
|
||||
if cmp := c.CompareString(part1, part2); cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
}
|
||||
pos1, pos2 = end1, end2
|
||||
}
|
||||
return len(s1) < len(s2)
|
||||
}
|
||||
|
||||
@@ -10,21 +10,36 @@ import (
|
||||
)
|
||||
|
||||
func TestNaturalSortLess(t *testing.T) {
|
||||
test := func(s1, s2 string, less bool) {
|
||||
assert.Equal(t, less, NaturalSortLess(s1, s2), "s1=%q, s2=%q", s1, s2)
|
||||
testLess := func(s1, s2 string) {
|
||||
assert.True(t, NaturalSortLess(s1, s2), "s1<s2 should be true: s1=%q, s2=%q", s1, s2)
|
||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
testEqual := func(s1, s2 string) {
|
||||
assert.False(t, NaturalSortLess(s1, s2), "s1<s2 should be false: s1=%q, s2=%q", s1, s2)
|
||||
assert.False(t, NaturalSortLess(s2, s1), "s2<s1 should be false: s1=%q, s2=%q", s1, s2)
|
||||
}
|
||||
test("v1.20.0", "v1.2.0", false)
|
||||
test("v1.20.0", "v1.29.0", true)
|
||||
test("v1.20.0", "v1.20.0", false)
|
||||
test("abc", "bcd", true)
|
||||
test("a-1-a", "a-1-b", true)
|
||||
test("2", "12", true)
|
||||
test("a", "ab", true)
|
||||
|
||||
test("A", "b", true)
|
||||
test("a", "B", true)
|
||||
testEqual("", "")
|
||||
testLess("", "a")
|
||||
testLess("", "1")
|
||||
|
||||
test("cafe", "café", true)
|
||||
test("café", "cafe", false)
|
||||
test("caff", "café", false)
|
||||
testLess("v1.2", "v1.2.0")
|
||||
testLess("v1.2.0", "v1.10.0")
|
||||
testLess("v1.20.0", "v1.29.0")
|
||||
testEqual("v1.20.0", "v1.20.0")
|
||||
|
||||
testLess("a", "A")
|
||||
testLess("a", "B")
|
||||
testLess("A", "b")
|
||||
testLess("A", "ab")
|
||||
|
||||
testLess("abc", "bcd")
|
||||
testLess("a-1-a", "a-1-b")
|
||||
testLess("2", "12")
|
||||
|
||||
testLess("cafe", "café")
|
||||
testLess("café", "caff")
|
||||
|
||||
testLess("A-2", "A-11")
|
||||
testLess("0.txt", "1.txt")
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
_ "gitea.com/go-chi/cache/memcache" //nolint:depguard // memcache plugin for cache, it is required for config "ADAPTER=memcache"
|
||||
)
|
||||
|
||||
var defaultCache StringCache
|
||||
|
||||
+20
-8
@@ -57,11 +57,16 @@ func getForwardedHost(req *http.Request) string {
|
||||
return req.Header.Get("X-Forwarded-Host")
|
||||
}
|
||||
|
||||
// GuessCurrentAppURL tries to guess the current full URL by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
|
||||
// GuessCurrentAppURL tries to guess the current full app URL (with sub-path) by http headers. It always has a '/' suffix, exactly the same as setting.AppURL
|
||||
func GuessCurrentAppURL(ctx context.Context) string {
|
||||
return GuessCurrentHostURL(ctx) + setting.AppSubURL + "/"
|
||||
}
|
||||
|
||||
// GuessCurrentHostURL tries to guess the current full host URL (no sub-path) by http headers, there is no trailing slash.
|
||||
func GuessCurrentHostURL(ctx context.Context) string {
|
||||
req, ok := ctx.Value(RequestContextKey).(*http.Request)
|
||||
if !ok {
|
||||
return setting.AppURL
|
||||
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||
}
|
||||
// If no scheme provided by reverse proxy, then do not guess the AppURL, use the configured one.
|
||||
// At the moment, if site admin doesn't configure the proxy headers correctly, then Gitea would guess wrong.
|
||||
@@ -74,20 +79,27 @@ func GuessCurrentAppURL(ctx context.Context) string {
|
||||
// So in the future maybe it should introduce a new config option, to let site admin decide how to guess the AppURL.
|
||||
reqScheme := getRequestScheme(req)
|
||||
if reqScheme == "" {
|
||||
return setting.AppURL
|
||||
return strings.TrimSuffix(setting.AppURL, setting.AppSubURL+"/")
|
||||
}
|
||||
reqHost := getForwardedHost(req)
|
||||
if reqHost == "" {
|
||||
reqHost = req.Host
|
||||
}
|
||||
return reqScheme + "://" + reqHost + setting.AppSubURL + "/"
|
||||
return reqScheme + "://" + reqHost
|
||||
}
|
||||
|
||||
func MakeAbsoluteURL(ctx context.Context, s string) string {
|
||||
if IsRelativeURL(s) {
|
||||
return GuessCurrentAppURL(ctx) + strings.TrimPrefix(s, "/")
|
||||
// MakeAbsoluteURL tries to make a link to an absolute URL:
|
||||
// * If link is empty, it returns the current app URL.
|
||||
// * If link is absolute, it returns the link.
|
||||
// * Otherwise, it returns the current host URL + link, the link itself should have correct sub-path (AppSubURL) if needed.
|
||||
func MakeAbsoluteURL(ctx context.Context, link string) string {
|
||||
if link == "" {
|
||||
return GuessCurrentAppURL(ctx)
|
||||
}
|
||||
return s
|
||||
if !IsRelativeURL(link) {
|
||||
return link
|
||||
}
|
||||
return GuessCurrentHostURL(ctx) + "/" + strings.TrimPrefix(link, "/")
|
||||
}
|
||||
|
||||
func IsCurrentGiteaSiteURL(ctx context.Context, s string) bool {
|
||||
|
||||
@@ -46,14 +46,14 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
assert.Equal(t, "http://cfg-host/sub/", MakeAbsoluteURL(ctx, ""))
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "foo"))
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://other/foo", MakeAbsoluteURL(ctx, "http://other/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
})
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
@@ -61,7 +61,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
||||
"X-Forwarded-Host": {"forwarded-host"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "http://cfg-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "http://cfg-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
|
||||
ctx = context.WithValue(ctx, RequestContextKey, &http.Request{
|
||||
Host: "user-host",
|
||||
@@ -70,7 +70,7 @@ func TestMakeAbsoluteURL(t *testing.T) {
|
||||
"X-Forwarded-Proto": {"https"},
|
||||
},
|
||||
})
|
||||
assert.Equal(t, "https://forwarded-host/sub/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
assert.Equal(t, "https://forwarded-host/foo", MakeAbsoluteURL(ctx, "/foo"))
|
||||
}
|
||||
|
||||
func TestIsCurrentGiteaSiteURL(t *testing.T) {
|
||||
|
||||
@@ -38,6 +38,12 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
searchOpt.MilestoneIDs = opts.MilestoneIDs
|
||||
}
|
||||
|
||||
if opts.ProjectID > 0 {
|
||||
searchOpt.ProjectID = optional.Some(opts.ProjectID)
|
||||
} else if opts.ProjectID == -1 { // FIXME: this is inconsistent from other places
|
||||
searchOpt.ProjectID = optional.Some[int64](0) // Those issues with no project(projectid==0)
|
||||
}
|
||||
|
||||
// See the comment of issues_model.SearchOptions for the reason why we need to convert
|
||||
convertID := func(id int64) optional.Option[int64] {
|
||||
if id > 0 {
|
||||
@@ -49,7 +55,6 @@ func ToSearchOptions(keyword string, opts *issues_model.IssuesOptions) *SearchOp
|
||||
return nil
|
||||
}
|
||||
|
||||
searchOpt.ProjectID = convertID(opts.ProjectID)
|
||||
searchOpt.ProjectBoardID = convertID(opts.ProjectBoardID)
|
||||
searchOpt.PosterID = convertID(opts.PosterID)
|
||||
searchOpt.AssigneeID = convertID(opts.AssigneeID)
|
||||
|
||||
@@ -211,7 +211,7 @@ func createRequest(ctx context.Context, method, url string, headers map[string]s
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
req.Header.Set("Accept", MediaType)
|
||||
req.Header.Set("Accept", AcceptHeader)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -251,6 +251,6 @@ func handleErrorResponse(resp *http.Response) error {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Trace("ErrorResponse: %v", er)
|
||||
log.Trace("ErrorResponse(%v): %v", resp.Status, er)
|
||||
return errors.New(er.Message)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
||||
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, MediaType, req.Header.Get("Content-type"))
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
|
||||
var batchRequest BatchRequest
|
||||
err := json.NewDecoder(req.Body).Decode(&batchRequest)
|
||||
@@ -263,7 +263,7 @@ func TestHTTPClientUpload(t *testing.T) {
|
||||
hc := &http.Client{Transport: RoundTripFunc(func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
assert.Equal(t, MediaType, req.Header.Get("Content-type"))
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
|
||||
var batchRequest BatchRequest
|
||||
err := json.NewDecoder(req.Body).Decode(&batchRequest)
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
const (
|
||||
// MediaType contains the media type for LFS server requests
|
||||
MediaType = "application/vnd.git-lfs+json"
|
||||
// Some LFS servers offer content with other types, so fallback to '*/*' if application/vnd.git-lfs+json cannot be served
|
||||
AcceptHeader = "application/vnd.git-lfs+json;q=0.9, */*;q=0.8"
|
||||
)
|
||||
|
||||
// BatchRequest contains multiple requests processed in one batch operation.
|
||||
|
||||
@@ -37,6 +37,7 @@ func (a *BasicTransferAdapter) Download(ctx context.Context, l *Link) (io.ReadCl
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("Download Request: %+v", req)
|
||||
resp, err := performRequest(ctx, a.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestBasicTransferAdapter(t *testing.T) {
|
||||
p := Pointer{Oid: "b5a2c96250612366ea272ffac6d9744aaf4b45aacd96aa7cfcb931ee3b558259", Size: 5}
|
||||
|
||||
roundTripHandler := func(req *http.Request) *http.Response {
|
||||
assert.Equal(t, MediaType, req.Header.Get("Accept"))
|
||||
assert.Equal(t, AcceptHeader, req.Header.Get("Accept"))
|
||||
assert.Equal(t, "test-value", req.Header.Get("test-header"))
|
||||
|
||||
url := req.URL.String()
|
||||
|
||||
+20
-44
@@ -49,7 +49,7 @@ var (
|
||||
// hashCurrentPattern matches string that represents a commit SHA, e.g. d8a994ef243349f321568f9e36d5c3f444b99cae
|
||||
// Although SHA1 hashes are 40 chars long, SHA256 are 64, the regex matches the hash from 7 to 64 chars in length
|
||||
// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
|
||||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,](\s|$))`)
|
||||
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
|
||||
|
||||
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
||||
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||
@@ -88,6 +88,10 @@ func IsFullURLString(link string) bool {
|
||||
return fullURLPattern.MatchString(link)
|
||||
}
|
||||
|
||||
func IsNonEmptyRelativePath(link string) bool {
|
||||
return link != "" && !IsFullURLString(link) && link[0] != '/' && link[0] != '?' && link[0] != '#'
|
||||
}
|
||||
|
||||
// regexp for full links to issues/pulls
|
||||
var issueFullPattern *regexp.Regexp
|
||||
|
||||
@@ -372,7 +376,7 @@ func postProcess(ctx *RenderContext, procs []processor, input io.Reader, output
|
||||
return nil
|
||||
}
|
||||
|
||||
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
||||
func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Node {
|
||||
// Add user-content- to IDs and "#" links if they don't already have them
|
||||
for idx, attr := range node.Attr {
|
||||
val := strings.TrimPrefix(attr.Val, "#")
|
||||
@@ -391,27 +395,20 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// We ignore code and pre.
|
||||
switch node.Type {
|
||||
case html.TextNode:
|
||||
textNode(ctx, procs, node)
|
||||
case html.ElementNode:
|
||||
if node.Data == "img" {
|
||||
for i, attr := range node.Attr {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
if len(attr.Val) > 0 && !IsFullURLString(attr.Val) && !strings.HasPrefix(attr.Val, "data:image/") {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
node.Attr[i] = attr
|
||||
}
|
||||
if node.Data == "code" || node.Data == "pre" {
|
||||
// ignore code and pre nodes
|
||||
return node.NextSibling
|
||||
} else if node.Data == "img" {
|
||||
return visitNodeImg(ctx, node)
|
||||
} else if node.Data == "video" {
|
||||
return visitNodeVideo(ctx, node)
|
||||
} else if node.Data == "a" {
|
||||
// Restrict text in links to emojis
|
||||
procs = emojiProcessors
|
||||
} else if node.Data == "code" || node.Data == "pre" {
|
||||
return
|
||||
} else if node.Data == "i" {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key != "class" {
|
||||
@@ -434,11 +431,11 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) {
|
||||
}
|
||||
}
|
||||
}
|
||||
for n := node.FirstChild; n != nil; n = n.NextSibling {
|
||||
visitNode(ctx, procs, n)
|
||||
for n := node.FirstChild; n != nil; {
|
||||
n = visitNode(ctx, procs, n)
|
||||
}
|
||||
}
|
||||
// ignore everything else
|
||||
return node.NextSibling
|
||||
}
|
||||
|
||||
// textNode runs the passed node through various processors, in order to handle
|
||||
@@ -733,10 +730,10 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if image {
|
||||
link = strings.ReplaceAll(link, " ", "+")
|
||||
} else {
|
||||
link = strings.ReplaceAll(link, " ", "-")
|
||||
link = strings.ReplaceAll(link, " ", "-") // FIXME: it should support dashes in the link, eg: "the-dash-support.-"
|
||||
}
|
||||
if !strings.Contains(link, "/") {
|
||||
link = url.PathEscape(link)
|
||||
link = url.PathEscape(link) // FIXME: it doesn't seem right and it might cause double-escaping
|
||||
}
|
||||
}
|
||||
if image {
|
||||
@@ -768,28 +765,7 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
|
||||
childNode.Attr = childNode.Attr[:2]
|
||||
}
|
||||
} else {
|
||||
if !absoluteLink {
|
||||
var base string
|
||||
if ctx.IsWiki {
|
||||
switch ext {
|
||||
case "":
|
||||
// no file extension, create a regular wiki link
|
||||
base = ctx.Links.WikiLink()
|
||||
default:
|
||||
// we have a file extension:
|
||||
// return a regular wiki link if it's a renderable file (extension),
|
||||
// raw link otherwise
|
||||
if Type(link) != "" {
|
||||
base = ctx.Links.WikiLink()
|
||||
} else {
|
||||
base = ctx.Links.WikiRawLink()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
base = ctx.Links.SrcLink()
|
||||
}
|
||||
link = util.URLJoin(base, link)
|
||||
}
|
||||
link, _ = ResolveLink(ctx, link, "")
|
||||
childNode.Type = html.TextNode
|
||||
childNode.Data = name
|
||||
}
|
||||
@@ -851,7 +827,7 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
|
||||
// FIXME: the use of "mode" is quite dirty and hacky, for example: what is a "document"? how should it be rendered?
|
||||
// The "mode" approach should be refactored to some other more clear&reliable way.
|
||||
crossLinkOnly := (ctx.Metas["mode"] == "document" && !ctx.IsWiki)
|
||||
crossLinkOnly := ctx.Metas["mode"] == "document" && !ctx.IsWiki
|
||||
|
||||
var (
|
||||
found bool
|
||||
|
||||
@@ -18,8 +18,7 @@ import (
|
||||
|
||||
const (
|
||||
TestAppURL = "http://localhost:3000/"
|
||||
TestOrgRepo = "gogits/gogs"
|
||||
TestRepoURL = TestAppURL + TestOrgRepo + "/"
|
||||
TestRepoURL = TestAppURL + "test-owner/test-repo/"
|
||||
)
|
||||
|
||||
// externalIssueLink an HTML link to an alphanumeric-style issue
|
||||
@@ -64,8 +63,8 @@ var regexpMetas = map[string]string{
|
||||
|
||||
// these values should match the TestOrgRepo const above
|
||||
var localMetas = map[string]string{
|
||||
"user": "gogits",
|
||||
"repo": "gogs",
|
||||
"user": "test-owner",
|
||||
"repo": "test-repo",
|
||||
}
|
||||
|
||||
func TestRender_IssueIndexPattern(t *testing.T) {
|
||||
@@ -362,12 +361,12 @@ func TestRender_FullIssueURLs(t *testing.T) {
|
||||
`Look here <a href="http://localhost:3000/person/repo/issues/4" class="ref-issue">person/repo#4</a>`)
|
||||
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
|
||||
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a>`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4 test",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4" class="ref-issue">#4</a> test`)
|
||||
test("http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123 test",
|
||||
`<a href="http://localhost:3000/gogits/gogs/issues/4?a=1&b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a>`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4 test",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4" class="ref-issue">#4</a> test`)
|
||||
test("http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123 test",
|
||||
`<a href="http://localhost:3000/test-owner/test-repo/issues/4?a=1&b=2#comment-123" class="ref-issue">#4 (comment)</a> test`)
|
||||
test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24",
|
||||
"http://localhost:3000/testOrg/testOrgRepo/pulls/2/files#issuecomment-24")
|
||||
test("http://localhost:3000/testOrg/testOrgRepo/pulls/2/files",
|
||||
@@ -381,6 +380,7 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
|
||||
"(abcdefabcdefabcdefabcdefabcdefabcdefabcd)",
|
||||
"[abcdefabcdefabcdefabcdefabcdefabcdefabcd]",
|
||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd.",
|
||||
"abcdefabcdefabcdefabcdefabcdefabcdefabcd:",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"test",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func ResolveLink(ctx *RenderContext, link, userContentAnchorPrefix string) (result string, resolved bool) {
|
||||
isAnchorFragment := link != "" && link[0] == '#'
|
||||
if !isAnchorFragment && !IsFullURLString(link) {
|
||||
linkBase := ctx.Links.Base
|
||||
if ctx.IsWiki {
|
||||
if ext := path.Ext(link); ext == "" || ext == ".-" {
|
||||
linkBase = ctx.Links.WikiLink() // the link is for a wiki page
|
||||
} else if DetectMarkupTypeByFileName(link) != "" {
|
||||
linkBase = ctx.Links.WikiLink() // the link is renderable as a wiki page
|
||||
} else {
|
||||
linkBase = ctx.Links.WikiRawLink() // otherwise, use a raw link instead to view&download medias
|
||||
}
|
||||
} else if ctx.Links.BranchPath != "" || ctx.Links.TreePath != "" {
|
||||
// if there is no BranchPath, then the link will be something like "/owner/repo/src/{the-file-path}"
|
||||
// and then this link will be handled by the "legacy-ref" code and be redirected to the default branch like "/owner/repo/src/branch/main/{the-file-path}"
|
||||
linkBase = ctx.Links.SrcLink()
|
||||
}
|
||||
link, resolved = util.URLJoin(linkBase, link), true
|
||||
}
|
||||
if isAnchorFragment && userContentAnchorPrefix != "" {
|
||||
link, resolved = userContentAnchorPrefix+link[1:], true
|
||||
}
|
||||
return link, resolved
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func visitNodeImg(ctx *RenderContext, img *html.Node) (next *html.Node) {
|
||||
next = img.NextSibling
|
||||
for i, attr := range img.Attr {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
||||
|
||||
// By default, the "<img>" tag should also be clickable,
|
||||
// because frontend use `<img>` to paste the re-scaled image into the markdown,
|
||||
// so it must match the default markdown image behavior.
|
||||
hasParentAnchor := false
|
||||
for p := img.Parent; p != nil; p = p.Parent {
|
||||
if hasParentAnchor = p.Type == html.ElementNode && p.Data == "a"; hasParentAnchor {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasParentAnchor {
|
||||
imgA := &html.Node{Type: html.ElementNode, Data: "a", Attr: []html.Attribute{
|
||||
{Key: "href", Val: attr.Val},
|
||||
{Key: "target", Val: "_blank"},
|
||||
}}
|
||||
parent := img.Parent
|
||||
imgNext := img.NextSibling
|
||||
parent.RemoveChild(img)
|
||||
parent.InsertBefore(imgA, imgNext)
|
||||
imgA.AppendChild(img)
|
||||
}
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
img.Attr[i] = attr
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func visitNodeVideo(ctx *RenderContext, node *html.Node) (next *html.Node) {
|
||||
next = node.NextSibling
|
||||
for i, attr := range node.Attr {
|
||||
if attr.Key != "src" {
|
||||
continue
|
||||
}
|
||||
if IsNonEmptyRelativePath(attr.Val) {
|
||||
attr.Val = util.URLJoin(ctx.Links.ResolveMediaLink(ctx.IsWiki), attr.Val)
|
||||
}
|
||||
attr.Val = camoHandleLink(attr.Val)
|
||||
node.Attr[i] = attr
|
||||
}
|
||||
return next
|
||||
}
|
||||
+43
-41
@@ -53,7 +53,7 @@ func TestRender_Commits(t *testing.T) {
|
||||
}
|
||||
|
||||
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
|
||||
repo := markup.TestRepoURL
|
||||
repo := "http://localhost:3000/gogits/gogs"
|
||||
commit := util.URLJoin(repo, "commit", sha)
|
||||
tree := util.URLJoin(repo, "tree", sha, "src")
|
||||
|
||||
@@ -107,8 +107,8 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
}
|
||||
|
||||
test(
|
||||
"gogits/gogs#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogits", "gogs", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogits/gogs#12345</a></p>`)
|
||||
"test-owner/test-repo#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "test-owner", "test-repo", "issues", "12345")+`" class="ref-issue" rel="nofollow">test-owner/test-repo#12345</a></p>`)
|
||||
test(
|
||||
"go-gitea/gitea#12345",
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "go-gitea", "gitea", "issues", "12345")+`" class="ref-issue" rel="nofollow">go-gitea/gitea#12345</a></p>`)
|
||||
@@ -156,13 +156,18 @@ func TestRender_links(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer))
|
||||
}
|
||||
// Text that should be turned into URL
|
||||
|
||||
defaultCustom := setting.Markdown.CustomURLSchemes
|
||||
oldCustomURLSchemes := setting.Markdown.CustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
defer func() {
|
||||
setting.Markdown.CustomURLSchemes = oldCustomURLSchemes
|
||||
markup.ResetDefaultSanitizerForTesting()
|
||||
markup.CustomLinkURLSchemes(oldCustomURLSchemes)
|
||||
}()
|
||||
setting.Markdown.CustomURLSchemes = []string{"ftp", "magnet"}
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
|
||||
// Text that should be turned into URL
|
||||
test(
|
||||
"https://www.example.com",
|
||||
`<p><a href="https://www.example.com" rel="nofollow">https://www.example.com</a></p>`)
|
||||
@@ -246,11 +251,6 @@ func TestRender_links(t *testing.T) {
|
||||
test(
|
||||
"ftps://gitea.com",
|
||||
`<p>ftps://gitea.com</p>`)
|
||||
|
||||
// Restore previous settings
|
||||
setting.Markdown.CustomURLSchemes = defaultCustom
|
||||
markup.InitializeSanitizer()
|
||||
markup.CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
|
||||
func TestRender_email(t *testing.T) {
|
||||
@@ -442,6 +442,10 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||
"[[Link]]",
|
||||
`<p><a href="`+url+`" rel="nofollow">Link</a></p>`,
|
||||
`<p><a href="`+urlWiki+`" rel="nofollow">Link</a></p>`)
|
||||
test(
|
||||
"[[Link.-]]",
|
||||
`<p><a href="http://localhost:3000/test-owner/test-repo/src/master/Link.-" rel="nofollow">Link.-</a></p>`,
|
||||
`<p><a href="http://localhost:3000/test-owner/test-repo/wiki/Link.-" rel="nofollow">Link.-</a></p>`)
|
||||
test(
|
||||
"[[Link.jpg]]",
|
||||
`<p><a href="`+imgurl+`" rel="nofollow"><img src="`+imgurl+`" title="Link.jpg" alt="Link.jpg"/></a></p>`,
|
||||
@@ -516,44 +520,41 @@ func TestRender_ShortLinks(t *testing.T) {
|
||||
`<p><a href="https://example.org" rel="nofollow">[[foobar]]</a></p>`)
|
||||
}
|
||||
|
||||
func TestRender_RelativeImages(t *testing.T) {
|
||||
setting.AppURL = markup.TestAppURL
|
||||
|
||||
test := func(input, expected, expectedWiki string) {
|
||||
func TestRender_RelativeMedias(t *testing.T) {
|
||||
render := func(input string, isWiki bool, links markup.Links) string {
|
||||
buffer, err := markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
BranchPath: "master",
|
||||
},
|
||||
Metas: localMetas,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(buffer)))
|
||||
buffer, err = markdown.RenderString(&markup.RenderContext{
|
||||
Ctx: git.DefaultContext,
|
||||
Links: markup.Links{
|
||||
Base: markup.TestRepoURL,
|
||||
},
|
||||
Ctx: git.DefaultContext,
|
||||
Links: links,
|
||||
Metas: localMetas,
|
||||
IsWiki: true,
|
||||
IsWiki: isWiki,
|
||||
}, input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(string(buffer)))
|
||||
return strings.TrimSpace(string(buffer))
|
||||
}
|
||||
|
||||
rawwiki := util.URLJoin(markup.TestRepoURL, "wiki", "raw")
|
||||
mediatree := util.URLJoin(markup.TestRepoURL, "media", "master")
|
||||
out := render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/LINK"/></a>`, out)
|
||||
|
||||
test(
|
||||
`<img src="Link">`,
|
||||
`<img src="`+util.URLJoin(mediatree, "Link")+`"/>`,
|
||||
`<img src="`+util.URLJoin(rawwiki, "Link")+`"/>`)
|
||||
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
|
||||
|
||||
test(
|
||||
`<img src="./icon.png">`,
|
||||
`<img src="`+util.URLJoin(mediatree, "icon.png")+`"/>`,
|
||||
`<img src="`+util.URLJoin(rawwiki, "icon.png")+`"/>`)
|
||||
out = render(`<img src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/media/test-branch/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/media/test-branch/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<a href="/test-owner/test-repo/wiki/raw/LINK" target="_blank" rel="nofollow noopener"><img src="/test-owner/test-repo/wiki/raw/LINK"/></a>`, out)
|
||||
|
||||
out = render(`<img src="/LINK">`, true, markup.Links{Base: "/test-owner/test-repo", BranchPath: "test-branch"})
|
||||
assert.Equal(t, `<img src="/LINK"/>`, out)
|
||||
|
||||
out = render(`<video src="LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/test-owner/test-repo/LINK"></video>`, out)
|
||||
|
||||
out = render(`<video src="LINK">`, true, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/test-owner/test-repo/wiki/raw/LINK"></video>`, out)
|
||||
|
||||
out = render(`<video src="/LINK">`, false, markup.Links{Base: "/test-owner/test-repo"})
|
||||
assert.Equal(t, `<video src="/LINK"></video>`, out)
|
||||
}
|
||||
|
||||
func Test_ParseClusterFuzz(t *testing.T) {
|
||||
@@ -706,5 +707,6 @@ func TestIssue18471(t *testing.T) {
|
||||
func TestIsFullURL(t *testing.T) {
|
||||
assert.True(t, markup.IsFullURLString("https://example.com"))
|
||||
assert.True(t, markup.IsFullURLString("mailto:test@example.com"))
|
||||
assert.True(t, markup.IsFullURLString("data:image/11111"))
|
||||
assert.False(t, markup.IsFullURLString("/foo:bar"))
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
||||
case *ast.Image:
|
||||
g.transformImage(ctx, v, reader)
|
||||
case *ast.Link:
|
||||
g.transformLink(ctx, v, reader)
|
||||
g.transformLink(ctx, v)
|
||||
case *ast.List:
|
||||
g.transformList(ctx, v, reader, rc)
|
||||
case *ast.Text:
|
||||
|
||||
@@ -542,6 +542,10 @@ func TestMathBlock(t *testing.T) {
|
||||
"$$a$$",
|
||||
`<pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre>` + nl,
|
||||
},
|
||||
{
|
||||
"$a$ ($b$) [$c$] {$d$}",
|
||||
`<p><code class="language-math is-loading">a</code> (<code class="language-math is-loading">b</code>) [$c$] {$d$}</p>` + nl,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
@@ -626,7 +630,7 @@ mail@domain.com
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/src/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/path/file" target="_blank" rel="nofollow noopener"><img src="/path/file" alt="local image"/></a><br/>
|
||||
@@ -682,7 +686,7 @@ space</p>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/src/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://gitea.io/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="https://gitea.io/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="https://gitea.io/path/file" target="_blank" rel="nofollow noopener"><img src="https://gitea.io/path/file" alt="local image"/></a><br/>
|
||||
@@ -740,7 +744,7 @@ space</p>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/src/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="/relative/path/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/relative/path/image.jpg" target="_blank" rel="nofollow noopener"><img src="/relative/path/image.jpg" alt="local image"/></a><br/>
|
||||
<a href="/relative/path/path/file" target="_blank" rel="nofollow noopener"><img src="/relative/path/path/file" alt="local image"/></a><br/>
|
||||
@@ -857,7 +861,7 @@ space</p>
|
||||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
@@ -975,7 +979,7 @@ space</p>
|
||||
for i, c := range cases {
|
||||
result, err := markdown.RenderString(&markup.RenderContext{Ctx: context.Background(), Links: c.Links, IsWiki: c.IsWiki}, input)
|
||||
assert.NoError(t, err, "Unexpected error in testcase: %v", i)
|
||||
assert.Equal(t, template.HTML(c.Expected), result, "Unexpected result in testcase %v", i)
|
||||
assert.Equal(t, c.Expected, string(result), "Unexpected result in testcase %v", i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1010,4 +1014,10 @@ func TestAttention(t *testing.T) {
|
||||
test(`> [!important]`, renderAttention("important", "octicon-report")+"\n</blockquote>")
|
||||
test(`> [!warning]`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
test(`> [!caution]`, renderAttention("caution", "octicon-stop")+"\n</blockquote>")
|
||||
|
||||
// escaped by mdformat
|
||||
test(`> \[!NOTE\]`, renderAttention("note", "octicon-info")+"\n</blockquote>")
|
||||
|
||||
// legacy GitHub style
|
||||
test(`> **warning**`, renderAttention("warning", "octicon-alert")+"\n</blockquote>")
|
||||
}
|
||||
|
||||
@@ -31,10 +31,16 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
dollars := false
|
||||
var dollars bool
|
||||
if b.parseDollars && line[pos] == '$' && line[pos+1] == '$' {
|
||||
dollars = true
|
||||
} else if line[pos] != '\\' || line[pos+1] != '[' {
|
||||
} else if line[pos] == '\\' && line[pos+1] == '[' {
|
||||
if len(line[pos:]) >= 3 && line[pos+2] == '!' && bytes.Contains(line[pos:], []byte(`\]`)) {
|
||||
// do not process escaped attention block: "> \[!NOTE\]"
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
dollars = false
|
||||
} else {
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ func isPunctuation(b byte) bool {
|
||||
return b == '.' || b == '!' || b == '?' || b == ',' || b == ';' || b == ':'
|
||||
}
|
||||
|
||||
func isBracket(b byte) bool {
|
||||
return b == ')'
|
||||
}
|
||||
|
||||
func isAlphanumeric(b byte) bool {
|
||||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
||||
}
|
||||
@@ -84,7 +88,7 @@ func (parser *inlineParser) Parse(parent ast.Node, block text.Reader, pc parser.
|
||||
break
|
||||
}
|
||||
suceedingCharacter := line[pos]
|
||||
if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') {
|
||||
if !isPunctuation(suceedingCharacter) && !(suceedingCharacter == ' ') && !isBracket(suceedingCharacter) {
|
||||
return nil
|
||||
}
|
||||
if line[ender-1] != '\\' {
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
|
||||
// renderAttention renders a quote marked with i.e. "> **Note**" or "> [!Warning]" with a corresponding svg
|
||||
func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
n := node.(*Attention)
|
||||
@@ -37,38 +37,93 @@ func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
||||
// We only want attention blockquotes when the AST looks like:
|
||||
// > Text("[") Text("!TYPE") Text("]")
|
||||
func (g *ASTTransformer) extractBlockquoteAttentionEmphasis(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 1 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Emphasis)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Text(reader.Source()))
|
||||
attentionType := strings.ToLower(val1)
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// grab these nodes and make sure we adhere to the attention blockquote structure
|
||||
firstParagraph := v.FirstChild()
|
||||
g.applyElementDir(firstParagraph)
|
||||
if firstParagraph.ChildCount() < 3 {
|
||||
return ast.WalkContinue, nil
|
||||
func (g *ASTTransformer) extractBlockquoteAttention2(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 2 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
node2, ok := node1.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Segment.Value(reader.Source()))
|
||||
val2 := string(node2.Segment.Value(reader.Source()))
|
||||
if strings.HasPrefix(val1, `\[!`) && val2 == `\]` {
|
||||
attentionType := strings.ToLower(val1[3:])
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1, node2}
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) extractBlockquoteAttention3(firstParagraph ast.Node, reader text.Reader) (string, []ast.Node) {
|
||||
if firstParagraph.ChildCount() < 3 {
|
||||
return "", nil
|
||||
}
|
||||
node1, ok := firstParagraph.FirstChild().(*ast.Text)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
node2, ok := node1.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
node3, ok := node2.NextSibling().(*ast.Text)
|
||||
if !ok {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
val1 := string(node1.Segment.Value(reader.Source()))
|
||||
val2 := string(node2.Segment.Value(reader.Source()))
|
||||
val3 := string(node3.Segment.Value(reader.Source()))
|
||||
if val1 != "[" || val3 != "]" || !strings.HasPrefix(val2, "!") {
|
||||
return ast.WalkContinue, nil
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// grab attention type from markdown source
|
||||
attentionType := strings.ToLower(val2[1:])
|
||||
if !g.attentionTypes.Contains(attentionType) {
|
||||
if g.attentionTypes.Contains(attentionType) {
|
||||
return attentionType, []ast.Node{node1, node2, node3}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Reader) (ast.WalkStatus, error) {
|
||||
// We only want attention blockquotes when the AST looks like:
|
||||
// > Text("[") Text("!TYPE") Text("]")
|
||||
// > Text("\[!TYPE") TEXT("\]")
|
||||
// > Text("**TYPE**")
|
||||
|
||||
// grab these nodes and make sure we adhere to the attention blockquote structure
|
||||
firstParagraph := v.FirstChild()
|
||||
g.applyElementDir(firstParagraph)
|
||||
|
||||
attentionType, processedNodes := g.extractBlockquoteAttentionEmphasis(firstParagraph, reader)
|
||||
if attentionType == "" {
|
||||
attentionType, processedNodes = g.extractBlockquoteAttention2(firstParagraph, reader)
|
||||
}
|
||||
if attentionType == "" {
|
||||
attentionType, processedNodes = g.extractBlockquoteAttention3(firstParagraph, reader)
|
||||
}
|
||||
if attentionType == "" {
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
@@ -88,9 +143,9 @@ func (g *ASTTransformer) transformBlockquote(v *ast.Blockquote, reader text.Read
|
||||
attentionParagraph.AppendChild(attentionParagraph, NewAttention(attentionType))
|
||||
attentionParagraph.AppendChild(attentionParagraph, emphasis)
|
||||
firstParagraph.Parent().InsertBefore(firstParagraph.Parent(), firstParagraph, attentionParagraph)
|
||||
firstParagraph.RemoveChild(firstParagraph, node1)
|
||||
firstParagraph.RemoveChild(firstParagraph, node2)
|
||||
firstParagraph.RemoveChild(firstParagraph, node3)
|
||||
for _, processed := range processedNodes {
|
||||
firstParagraph.RemoveChild(firstParagraph, processed)
|
||||
}
|
||||
if firstParagraph.ChildCount() == 0 {
|
||||
firstParagraph.Parent().RemoveChild(firstParagraph.Parent(), firstParagraph)
|
||||
}
|
||||
|
||||
@@ -4,39 +4,13 @@
|
||||
package markdown
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
giteautil "code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/yuin/goldmark/ast"
|
||||
"github.com/yuin/goldmark/text"
|
||||
)
|
||||
|
||||
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link, reader text.Reader) {
|
||||
// Links need their href to munged to be a real value
|
||||
link := v.Destination
|
||||
isAnchorFragment := len(link) > 0 && link[0] == '#'
|
||||
if !isAnchorFragment && !markup.IsFullURLBytes(link) {
|
||||
base := ctx.Links.Base
|
||||
if ctx.IsWiki {
|
||||
if filepath.Ext(string(link)) == "" {
|
||||
// This link doesn't have a file extension - assume a regular wiki link
|
||||
base = ctx.Links.WikiLink()
|
||||
} else if markup.Type(string(link)) != "" {
|
||||
// If it's a file type we can render, use a regular wiki link
|
||||
base = ctx.Links.WikiLink()
|
||||
} else {
|
||||
// Otherwise, use a raw link instead
|
||||
base = ctx.Links.WikiRawLink()
|
||||
}
|
||||
} else if ctx.Links.HasBranchInfo() {
|
||||
base = ctx.Links.SrcLink()
|
||||
}
|
||||
link = []byte(giteautil.URLJoin(base, string(link)))
|
||||
func (g *ASTTransformer) transformLink(ctx *markup.RenderContext, v *ast.Link) {
|
||||
if link, resolved := markup.ResolveLink(ctx, string(v.Destination), "#user-content-"); resolved {
|
||||
v.Destination = []byte(link)
|
||||
}
|
||||
if isAnchorFragment {
|
||||
link = []byte("#user-content-" + string(link)[1:])
|
||||
}
|
||||
v.Destination = link
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ func Init(ph *ProcessorHelper) {
|
||||
DefaultProcessorHelper = *ph
|
||||
}
|
||||
|
||||
NewSanitizer()
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
CustomLinkURLSchemes(setting.Markdown.CustomURLSchemes)
|
||||
}
|
||||
@@ -74,7 +73,7 @@ type RenderContext struct {
|
||||
Type string
|
||||
IsWiki bool
|
||||
Links Links
|
||||
Metas map[string]string
|
||||
Metas map[string]string // user, repo, mode(comment/document)
|
||||
DefaultLink string
|
||||
GitRepo *git.Repository
|
||||
ShaExistCache map[string]bool
|
||||
@@ -85,10 +84,10 @@ type RenderContext struct {
|
||||
}
|
||||
|
||||
type Links struct {
|
||||
AbsolutePrefix bool
|
||||
Base string
|
||||
BranchPath string
|
||||
TreePath string
|
||||
AbsolutePrefix bool // add absolute URL prefix to auto-resolved links like "#issue", but not for pre-provided links and medias
|
||||
Base string // base prefix for pre-provided links and medias (images, videos)
|
||||
BranchPath string // actually it is the ref path, eg: "branch/features/feat-12", "tag/v1.0"
|
||||
TreePath string // the dir of the file, eg: "doc" if the file "doc/CHANGE.md" is being rendered
|
||||
}
|
||||
|
||||
func (l *Links) Prefix() string {
|
||||
@@ -371,22 +370,14 @@ func renderFile(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||
return ErrUnsupportedRenderExtension{extension}
|
||||
}
|
||||
|
||||
// Type returns if markup format via the filename
|
||||
func Type(filename string) string {
|
||||
// DetectMarkupTypeByFileName returns the possible markup format type via the filename
|
||||
func DetectMarkupTypeByFileName(filename string) string {
|
||||
if parser := GetRendererByFileName(filename); parser != nil {
|
||||
return parser.Name()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IsMarkupFile reports whether file is a markup type file
|
||||
func IsMarkupFile(name, markup string) bool {
|
||||
if parser := GetRendererByFileName(name); parser != nil {
|
||||
return parser.Name() == markup
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func PreviewableExtensions() []string {
|
||||
extensions := make([]string, 0, len(extRenderers))
|
||||
for extension := range extRenderers {
|
||||
|
||||
+21
-201
@@ -5,13 +5,9 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
@@ -21,211 +17,35 @@ type Sanitizer struct {
|
||||
defaultPolicy *bluemonday.Policy
|
||||
descriptionPolicy *bluemonday.Policy
|
||||
rendererPolicies map[string]*bluemonday.Policy
|
||||
init sync.Once
|
||||
allowAllRegex *regexp.Regexp
|
||||
}
|
||||
|
||||
var (
|
||||
sanitizer = &Sanitizer{}
|
||||
allowAllRegex = regexp.MustCompile(".+")
|
||||
defaultSanitizer *Sanitizer
|
||||
defaultSanitizerOnce sync.Once
|
||||
)
|
||||
|
||||
// NewSanitizer initializes sanitizer with allowed attributes based on settings.
|
||||
// Multiple calls to this function will only create one instance of Sanitizer during
|
||||
// entire application lifecycle.
|
||||
func NewSanitizer() {
|
||||
sanitizer.init.Do(func() {
|
||||
InitializeSanitizer()
|
||||
})
|
||||
}
|
||||
|
||||
// InitializeSanitizer (re)initializes the current sanitizer to account for changes in settings
|
||||
func InitializeSanitizer() {
|
||||
sanitizer.rendererPolicies = map[string]*bluemonday.Policy{}
|
||||
sanitizer.defaultPolicy = createDefaultPolicy()
|
||||
sanitizer.descriptionPolicy = createRepoDescriptionPolicy()
|
||||
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := createDefaultPolicy()
|
||||
addSanitizerRules(policy, sanitizerRules)
|
||||
sanitizer.rendererPolicies[name] = policy
|
||||
func GetDefaultSanitizer() *Sanitizer {
|
||||
defaultSanitizerOnce.Do(func() {
|
||||
defaultSanitizer = &Sanitizer{
|
||||
rendererPolicies: map[string]*bluemonday.Policy{},
|
||||
allowAllRegex: regexp.MustCompile(".+"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
func addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
for name, renderer := range renderers {
|
||||
sanitizerRules := renderer.SanitizerRules()
|
||||
if len(sanitizerRules) > 0 {
|
||||
policy := defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.addSanitizerRules(policy, sanitizerRules)
|
||||
defaultSanitizer.rendererPolicies[name] = policy
|
||||
}
|
||||
}
|
||||
}
|
||||
defaultSanitizer.defaultPolicy = defaultSanitizer.createDefaultPolicy()
|
||||
defaultSanitizer.descriptionPolicy = defaultSanitizer.createRepoDescriptionPolicy()
|
||||
})
|
||||
return defaultSanitizer
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.descriptionPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
NewSanitizer()
|
||||
return sanitizer.defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
NewSanitizer()
|
||||
policy, exist := sanitizer.rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = sanitizer.defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
func ResetDefaultSanitizerForTesting() {
|
||||
defaultSanitizer = nil
|
||||
defaultSanitizerOnce = sync.Once{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) addSanitizerRules(policy *bluemonday.Policy, rules []setting.MarkupSanitizerRule) {
|
||||
for _, rule := range rules {
|
||||
if rule.AllowDataURIImages {
|
||||
policy.AllowDataURIImages()
|
||||
}
|
||||
if rule.Element != "" {
|
||||
if rule.Regexp != nil {
|
||||
policy.AllowAttrs(rule.AllowAttr).Matching(rule.Regexp).OnElements(rule.Element)
|
||||
} else {
|
||||
policy.AllowAttrs(rule.AllowAttr).OnElements(rule.Element)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.UGCPolicy()
|
||||
|
||||
// For JS code copy and Mermaid loading state
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-block( is-loading)?$`)).OnElements("pre")
|
||||
|
||||
// For code preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-preview-[-\w]+( file-content)?$`)).Globally()
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-num$`)).OnElements("td")
|
||||
policy.AllowAttrs("data-line-number").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-code chroma$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^code-inner$`)).OnElements("div")
|
||||
|
||||
// For code preview (unicode escape)
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^file-view( unicode-escaped)?$`)).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^lines-escape$`)).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^toggle-escape-button btn interact-bg$`)).OnElements("a") // don't use button, button might submit a form
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ambiguous-code-point|escaped-code-point|broken-code-point)$`)).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^char$`)).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content", "data-escaped").OnElements("span")
|
||||
|
||||
// For color preview
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^color-preview$`)).OnElements("span")
|
||||
|
||||
// For attention
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-header attention-\w+$`)).OnElements("blockquote")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-\w+$`)).OnElements("strong")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^attention-icon attention-\w+ svg octicon-[\w-]+$`)).OnElements("svg")
|
||||
policy.AllowAttrs("viewBox", "width", "height", "aria-hidden").OnElements("svg")
|
||||
policy.AllowAttrs("fill-rule", "d").OnElements("path")
|
||||
|
||||
// For Chroma markdown plugin
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+( display)?( is-loading)?$`)).OnElements("code")
|
||||
|
||||
// Checkboxes
|
||||
policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input")
|
||||
policy.AllowAttrs("checked", "disabled", "data-source-position").OnElements("input")
|
||||
|
||||
// Custom URL-Schemes
|
||||
if len(setting.Markdown.CustomURLSchemes) > 0 {
|
||||
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)
|
||||
} else {
|
||||
policy.AllowURLSchemesMatching(st.allowAllRegex)
|
||||
|
||||
// Even if every scheme is allowed, these three are blocked for security reasons
|
||||
disallowScheme := func(*url.URL) bool {
|
||||
return false
|
||||
}
|
||||
policy.AllowURLSchemeWithCustomPolicy("javascript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("vbscript", disallowScheme)
|
||||
policy.AllowURLSchemeWithCustomPolicy("data", disallowScheme)
|
||||
}
|
||||
|
||||
// Allow classes for anchors
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`ref-issue( ref-external-issue)?`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
||||
// Allow icons
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
||||
// Allow 'color' and 'background-color' properties for the style attribute on text elements.
|
||||
policy.AllowStyles("color", "background-color").OnElements("span", "p")
|
||||
|
||||
// Allow generally safe attributes
|
||||
generalSafeAttrs := []string{
|
||||
"abbr", "accept", "accept-charset",
|
||||
"accesskey", "action", "align", "alt",
|
||||
"aria-describedby", "aria-hidden", "aria-label", "aria-labelledby",
|
||||
"axis", "border", "cellpadding", "cellspacing", "char",
|
||||
"charoff", "charset", "checked",
|
||||
"clear", "cols", "colspan", "color",
|
||||
"compact", "coords", "datetime", "dir",
|
||||
"disabled", "enctype", "for", "frame",
|
||||
"headers", "height", "hreflang",
|
||||
"hspace", "ismap", "label", "lang",
|
||||
"maxlength", "media", "method",
|
||||
"multiple", "name", "nohref", "noshade",
|
||||
"nowrap", "open", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope",
|
||||
"selected", "shape", "size", "span",
|
||||
"start", "summary", "tabindex", "target",
|
||||
"title", "type", "usemap", "valign", "value",
|
||||
"vspace", "width", "itemprop",
|
||||
}
|
||||
|
||||
generalSafeElements := []string{
|
||||
"h1", "h2", "h3", "h4", "h5", "h6", "h7", "h8", "br", "b", "i", "strong", "em", "a", "pre", "code", "img", "tt",
|
||||
"div", "ins", "del", "sup", "sub", "p", "ol", "ul", "table", "thead", "tbody", "tfoot", "blockquote", "label",
|
||||
"dl", "dt", "dd", "kbd", "q", "samp", "var", "hr", "ruby", "rt", "rp", "li", "tr", "td", "th", "s", "strike", "summary",
|
||||
"details", "caption", "figure", "figcaption",
|
||||
"abbr", "bdo", "cite", "dfn", "mark", "small", "span", "time", "video", "wbr",
|
||||
}
|
||||
|
||||
policy.AllowAttrs(generalSafeAttrs...).OnElements(generalSafeElements...)
|
||||
|
||||
policy.AllowAttrs("src", "autoplay", "controls").OnElements("video")
|
||||
|
||||
policy.AllowAttrs("itemscope", "itemtype").OnElements("div")
|
||||
|
||||
// FIXME: Need to handle longdesc in img but there is no easy way to do it
|
||||
|
||||
// Custom keyword markup
|
||||
defaultSanitizer.addSanitizerRules(policy, setting.ExternalSanitizerRules)
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// Sanitize takes a string that contains a HTML fragment or document and applies policy whitelist.
|
||||
func Sanitize(s string) string {
|
||||
return GetDefaultSanitizer().defaultPolicy.Sanitize(s)
|
||||
}
|
||||
|
||||
// SanitizeReader sanitizes a Reader
|
||||
func SanitizeReader(r io.Reader, renderer string, w io.Writer) error {
|
||||
policy, exist := GetDefaultSanitizer().rendererPolicies[renderer]
|
||||
if !exist {
|
||||
policy = GetDefaultSanitizer().defaultPolicy
|
||||
}
|
||||
return policy.SanitizeReaderToWriter(r, w)
|
||||
}
|
||||
@@ -5,18 +5,16 @@
|
||||
package markup
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_Sanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
func TestSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
// Regular
|
||||
`<a onblur="alert(secret)" href="http://www.google.com">Google</a>`, `<a href="http://www.google.com" rel="nofollow">Google</a>`,
|
||||
"<scrİpt><script>alert(document.domain)</script></scrİpt>", "<script>alert(document.domain)</script>",
|
||||
|
||||
// Code highlighting class
|
||||
`<code class="random string"></code>`, `<code></code>`,
|
||||
@@ -72,34 +70,3 @@ func Test_Sanitizer(t *testing.T) {
|
||||
assert.Equal(t, testCases[i+1], Sanitize(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
NewSanitizer()
|
||||
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeNonEscape(t *testing.T) {
|
||||
descStr := "<scrİpt><script>alert(document.domain)</script></scrİpt>"
|
||||
|
||||
output := template.HTML(Sanitize(descStr))
|
||||
if strings.Contains(string(output), "<script>") {
|
||||
t.Errorf("un-escaped <script> in output: %q", output)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
)
|
||||
|
||||
// createRepoDescriptionPolicy returns a minimal more strict policy that is used for
|
||||
// repository descriptions.
|
||||
func (st *Sanitizer) createRepoDescriptionPolicy() *bluemonday.Policy {
|
||||
policy := bluemonday.NewPolicy()
|
||||
policy.AllowStandardURLs()
|
||||
|
||||
// Allow italics and bold.
|
||||
policy.AllowElements("i", "b", "em", "strong")
|
||||
|
||||
// Allow code.
|
||||
policy.AllowElements("code")
|
||||
|
||||
// Allow links
|
||||
policy.AllowAttrs("href", "target", "rel").OnElements("a")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img", "span")
|
||||
policy.AllowAttrs("aria-label").OnElements("span")
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
// SanitizeDescription sanitizes the HTML generated for a repository description.
|
||||
func SanitizeDescription(s string) string {
|
||||
return GetDefaultSanitizer().descriptionPolicy.Sanitize(s)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDescriptionSanitizer(t *testing.T) {
|
||||
testCases := []string{
|
||||
`<h1>Title</h1>`, `Title`,
|
||||
`<img src='img.png' alt='image'>`, ``,
|
||||
`<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`, `<span class="emoji" aria-label="thumbs up">THUMBS UP</span>`,
|
||||
`<span style="color: red">Hello World</span>`, `<span>Hello World</span>`,
|
||||
`<br>`, ``,
|
||||
`<a href="https://example.com" target="_blank" rel="noopener noreferrer">https://example.com</a>`, `<a href="https://example.com" target="_blank" rel="noopener noreferrer nofollow">https://example.com</a>`,
|
||||
`<a href="data:1234">data</a>`, `data`,
|
||||
`<mark>Important!</mark>`, `Important!`,
|
||||
`<details>Click me! <summary>Nothing to see here.</summary></details>`, `Click me! Nothing to see here.`,
|
||||
`<input type="hidden">`, ``,
|
||||
`<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`, `<b>I</b> have a <i>strong</i> <strong>opinion</strong> about <em>this</em>.`,
|
||||
`Provides alternative <code>wg(8)</code> tool`, `Provides alternative <code>wg(8)</code> tool`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCases); i += 2 {
|
||||
assert.Equal(t, testCases[i+1], SanitizeDescription(testCases[i]))
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository,
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("UpdateRepository: %w", err)
|
||||
}
|
||||
repo.ObjectFormatName = objFmt.Name() // keep consistent with db
|
||||
|
||||
allBranches := container.Set[string]{}
|
||||
{
|
||||
|
||||
@@ -326,14 +326,14 @@ func LogStartupProblem(skip int, level log.Level, format string, args ...any) {
|
||||
|
||||
func deprecatedSetting(rootCfg ConfigProvider, oldSection, oldKey, newSection, newKey, version string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents, please use `[%s].%s` instead because this fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
LogStartupProblem(1, log.ERROR, "Deprecated config option `[%s].%s` is present, please use `[%s].%s` instead. This fallback will be/has been removed in %s", oldSection, oldKey, newSection, newKey, version)
|
||||
}
|
||||
}
|
||||
|
||||
// deprecatedSettingDB add a hint that the configuration has been moved to database but still kept in app.ini
|
||||
func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
if rootCfg.Section(oldSection).HasKey(oldKey) {
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` presents but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
LogStartupProblem(1, log.ERROR, "Deprecation: config option `[%s].%s` present but it won't take effect because it has been moved to admin panel -> config setting", oldSection, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ package setting
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -19,7 +18,6 @@ var (
|
||||
Storage *Storage
|
||||
Enabled bool
|
||||
ChunkedUploadPath string
|
||||
RegistryHost string
|
||||
|
||||
LimitTotalOwnerCount int64
|
||||
LimitTotalOwnerSize int64
|
||||
@@ -66,9 +64,6 @@ func loadPackagesFrom(rootCfg ConfigProvider) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
appURL, _ := url.Parse(AppURL)
|
||||
Packages.RegistryHost = appURL.Host
|
||||
|
||||
Packages.ChunkedUploadPath = filepath.ToSlash(sec.Key("CHUNKED_UPLOAD_PATH").MustString("tmp/package-upload"))
|
||||
if !filepath.IsAbs(Packages.ChunkedUploadPath) {
|
||||
Packages.ChunkedUploadPath = filepath.ToSlash(filepath.Join(AppDataPath, Packages.ChunkedUploadPath))
|
||||
|
||||
@@ -25,7 +25,8 @@ type MarkupOption struct {
|
||||
//
|
||||
// in: body
|
||||
Mode string
|
||||
// Context to render
|
||||
// URL path for rendering issue, media and file links
|
||||
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
|
||||
//
|
||||
// in: body
|
||||
Context string
|
||||
@@ -53,7 +54,8 @@ type MarkdownOption struct {
|
||||
//
|
||||
// in: body
|
||||
Mode string
|
||||
// Context to render
|
||||
// URL path for rendering issue, media and file links
|
||||
// Expected format: /subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}
|
||||
//
|
||||
// in: body
|
||||
Context string
|
||||
|
||||
@@ -24,7 +24,7 @@ type Team struct {
|
||||
// CreateTeamOption options for creating a team
|
||||
type CreateTeamOption struct {
|
||||
// required: true
|
||||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(30)"`
|
||||
Name string `json:"name" binding:"Required;AlphaDashDot;MaxSize(255)"`
|
||||
Description string `json:"description" binding:"MaxSize(255)"`
|
||||
IncludesAllRepositories bool `json:"includes_all_repositories"`
|
||||
// enum: read,write,admin
|
||||
@@ -40,7 +40,7 @@ type CreateTeamOption struct {
|
||||
// EditTeamOption options for editing a team
|
||||
type EditTeamOption struct {
|
||||
// required: true
|
||||
Name string `json:"name" binding:"AlphaDashDot;MaxSize(30)"`
|
||||
Name string `json:"name" binding:"AlphaDashDot;MaxSize(255)"`
|
||||
Description *string `json:"description" binding:"MaxSize(255)"`
|
||||
IncludesAllRepositories *bool `json:"includes_all_repositories"`
|
||||
// enum: read,write,admin
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -237,8 +238,8 @@ func DotEscape(raw string) string {
|
||||
|
||||
// Iif is an "inline-if", similar util.Iif[T] but templates need the non-generic version,
|
||||
// and it could be simply used as "{{Iif expr trueVal}}" (omit the falseVal).
|
||||
func Iif(condition bool, vals ...any) any {
|
||||
if condition {
|
||||
func Iif(condition any, vals ...any) any {
|
||||
if isTemplateTruthy(condition) {
|
||||
return vals[0]
|
||||
} else if len(vals) > 1 {
|
||||
return vals[1]
|
||||
@@ -246,6 +247,32 @@ func Iif(condition bool, vals ...any) any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isTemplateTruthy(v any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Bool:
|
||||
return rv.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int() != 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return rv.Uint() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return rv.Float() != 0
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return rv.Complex() != 0
|
||||
case reflect.String, reflect.Slice, reflect.Array, reflect.Map:
|
||||
return rv.Len() > 0
|
||||
case reflect.Struct:
|
||||
return true
|
||||
default:
|
||||
return !rv.IsNil()
|
||||
}
|
||||
}
|
||||
|
||||
// Eval the expression and return the result, see the comment of eval.Expr for details.
|
||||
// To use this helper function in templates, pass each token as a separate parameter.
|
||||
//
|
||||
|
||||
@@ -5,8 +5,11 @@ package templates
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -65,3 +68,41 @@ func TestHTMLFormat(t *testing.T) {
|
||||
func TestSanitizeHTML(t *testing.T) {
|
||||
assert.Equal(t, template.HTML(`<a href="/" rel="nofollow">link</a> xss <div>inline</div>`), SanitizeHTML(`<a href="/">link</a> <a href="javascript:">xss</a> <div style="dangerous">inline</div>`))
|
||||
}
|
||||
|
||||
func TestTemplateTruthy(t *testing.T) {
|
||||
tmpl := template.New("test")
|
||||
tmpl.Funcs(template.FuncMap{"Iif": Iif})
|
||||
template.Must(tmpl.Parse(`{{if .Value}}true{{else}}false{{end}}:{{Iif .Value "true" "false"}}`))
|
||||
|
||||
cases := []any{
|
||||
nil, false, true, "", "string", 0, 1,
|
||||
byte(0), byte(1), int64(0), int64(1), float64(0), float64(1),
|
||||
complex(0, 0), complex(1, 0),
|
||||
(chan int)(nil), make(chan int),
|
||||
(func())(nil), func() {},
|
||||
util.ToPointer(0), util.ToPointer(util.ToPointer(0)),
|
||||
util.ToPointer(1), util.ToPointer(util.ToPointer(1)),
|
||||
[0]int{},
|
||||
[1]int{0},
|
||||
[]int(nil),
|
||||
[]int{},
|
||||
[]int{0},
|
||||
map[any]any(nil),
|
||||
map[any]any{},
|
||||
map[any]any{"k": "v"},
|
||||
(*struct{})(nil),
|
||||
struct{}{},
|
||||
util.ToPointer(struct{}{}),
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
truthyCount := 0
|
||||
for i, v := range cases {
|
||||
w.Reset()
|
||||
assert.NoError(t, tmpl.Execute(w, struct{ Value any }{v}), "case %d (%T) %#v fails", i, v, v)
|
||||
out := w.String()
|
||||
truthyCount += util.Iif(out == "true:true", 1, 0)
|
||||
truthyMatches := out == "true:true" || out == "false:false"
|
||||
assert.True(t, truthyMatches, "case %d (%T) %#v fail: %s", i, v, v, out)
|
||||
}
|
||||
assert.True(t, truthyCount != 0 && truthyCount != len(cases))
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestRenderMarkdownToHtml(t *testing.T) {
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a>
|
||||
<a href="/file.bin" rel="nofollow">local link</a>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a>
|
||||
<a href="/src/file.bin" rel="nofollow">local link</a>
|
||||
<a href="/file.bin" rel="nofollow">local link</a>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a>
|
||||
<a href="/image.jpg" target="_blank" rel="nofollow noopener"><img src="/image.jpg" alt="local image"/></a>
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
|
||||
@@ -190,7 +190,7 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||
#123
|
||||
space</p>
|
||||
`
|
||||
assert.EqualValues(t, expected, RenderMarkdownToHtml(context.Background(), testInput()))
|
||||
assert.Equal(t, expected, string(RenderMarkdownToHtml(context.Background(), testInput())))
|
||||
}
|
||||
|
||||
func TestRenderLabels(t *testing.T) {
|
||||
|
||||
@@ -34,8 +34,10 @@ func IsNormalPageCompleted(s string) bool {
|
||||
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
|
||||
}
|
||||
|
||||
func MockVariableValue[T any](p *T, v T) (reset func()) {
|
||||
func MockVariableValue[T any](p *T, v ...T) (reset func()) {
|
||||
old := *p
|
||||
*p = v
|
||||
if len(v) > 0 {
|
||||
*p = v[0]
|
||||
}
|
||||
return func() { *p = old }
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ func GetSiteCookie(req *http.Request, name string) string {
|
||||
|
||||
// SetSiteCookie returns given cookie value from request header.
|
||||
func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
|
||||
// Previous versions would use a cookie path with a trailing /.
|
||||
// These are more specific than cookies without a trailing /, so
|
||||
// we need to delete these if they exist.
|
||||
deleteLegacySiteCookie(resp, name)
|
||||
cookie := &http.Cookie{
|
||||
Name: name,
|
||||
Value: url.QueryEscape(value),
|
||||
@@ -46,10 +50,6 @@ func SetSiteCookie(resp http.ResponseWriter, name, value string, maxAge int) {
|
||||
SameSite: setting.SessionConfig.SameSite,
|
||||
}
|
||||
resp.Header().Add("Set-Cookie", cookie.String())
|
||||
// Previous versions would use a cookie path with a trailing /.
|
||||
// These are more specific than cookies without a trailing /, so
|
||||
// we need to delete these if they exist.
|
||||
deleteLegacySiteCookie(resp, name)
|
||||
}
|
||||
|
||||
// deleteLegacySiteCookie deletes the cookie with the given name at the cookie
|
||||
|
||||
@@ -1554,9 +1554,9 @@ issues.no_content = No description provided.
|
||||
issues.close = Close Issue
|
||||
issues.comment_pull_merged_at = merged commit %[1]s into %[2]s %[3]s
|
||||
issues.comment_manually_pull_merged_at = manually merged commit %[1]s into %[2]s %[3]s
|
||||
issues.close_comment_issue = Comment and Close
|
||||
issues.close_comment_issue = Close with Comment
|
||||
issues.reopen_issue = Reopen
|
||||
issues.reopen_comment_issue = Comment and Reopen
|
||||
issues.reopen_comment_issue = Reopen with Comment
|
||||
issues.create_comment = Comment
|
||||
issues.comment.blocked_user = Cannot create or edit comment because you are blocked by the poster or repository owner.
|
||||
issues.closed_at = `closed this issue <a id="%[1]s" href="#%[1]s">%[2]s</a>`
|
||||
|
||||
@@ -39,7 +39,7 @@ func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||
r = io.TeeReader(r, hasher)
|
||||
}
|
||||
// save chunk to storage
|
||||
writtenSize, err := st.Save(storagePath, r, -1)
|
||||
writtenSize, err := st.Save(storagePath, r, contentSize)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("save chunk to storage error: %v", err)
|
||||
}
|
||||
@@ -208,7 +208,7 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
|
||||
|
||||
// save merged file
|
||||
storagePath := fmt.Sprintf("%d/%d/%d.%s", artifact.RunID%255, artifact.ID%255, time.Now().UnixNano(), extension)
|
||||
written, err := st.Save(storagePath, mergedReader, -1)
|
||||
written, err := st.Save(storagePath, mergedReader, artifact.FileCompressedSize)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save merged file error: %v", err)
|
||||
}
|
||||
|
||||
@@ -116,9 +116,9 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
|
||||
}
|
||||
|
||||
func apiUnauthorizedError(ctx *context.Context) {
|
||||
// TODO: it doesn't seem quite right but it doesn't really cause problem at the moment.
|
||||
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed, ideally.
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+httplib.GuessCurrentAppURL(ctx)+`v2/token",service="container_registry",scope="*"`)
|
||||
// container registry requires that the "/v2" must be in the root, so the sub-path in AppURL should be removed
|
||||
realmURL := httplib.GuessCurrentHostURL(ctx) + "/v2/token"
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+realmURL+`",service="container_registry",scope="*"`)
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,20 +96,34 @@ func FeedCapabilityResource(ctx *context.Context) {
|
||||
xmlResponse(ctx, http.StatusOK, Metadata)
|
||||
}
|
||||
|
||||
var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
|
||||
var (
|
||||
searchTermExtract = regexp.MustCompile(`'([^']+)'`)
|
||||
searchTermExact = regexp.MustCompile(`\s+eq\s+'`)
|
||||
)
|
||||
|
||||
func getSearchTerm(ctx *context.Context) string {
|
||||
func getSearchTerm(ctx *context.Context) packages_model.SearchValue {
|
||||
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
|
||||
if searchTerm == "" {
|
||||
// $filter contains a query like:
|
||||
// (((Id ne null) and substringof('microsoft',tolower(Id)))
|
||||
// We don't support these queries, just extract the search term.
|
||||
match := searchTermExtract.FindStringSubmatch(ctx.FormTrim("$filter"))
|
||||
if len(match) == 2 {
|
||||
searchTerm = strings.TrimSpace(match[1])
|
||||
if searchTerm != "" {
|
||||
return packages_model.SearchValue{
|
||||
Value: searchTerm,
|
||||
ExactMatch: false,
|
||||
}
|
||||
}
|
||||
return searchTerm
|
||||
|
||||
// $filter contains a query like:
|
||||
// (((Id ne null) and substringof('microsoft',tolower(Id)))
|
||||
// https://www.odata.org/documentation/odata-version-2-0/uri-conventions/ section 4.5
|
||||
// We don't support these queries, just extract the search term.
|
||||
filter := ctx.FormTrim("$filter")
|
||||
match := searchTermExtract.FindStringSubmatch(filter)
|
||||
if len(match) == 2 {
|
||||
return packages_model.SearchValue{
|
||||
Value: strings.TrimSpace(match[1]),
|
||||
ExactMatch: searchTermExact.MatchString(filter),
|
||||
}
|
||||
}
|
||||
|
||||
return packages_model.SearchValue{}
|
||||
}
|
||||
|
||||
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||
@@ -118,11 +132,9 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
paginator := db.NewAbsoluteListOptions(skip, take)
|
||||
|
||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: getSearchTerm(ctx),
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
@@ -169,10 +181,8 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||
func SearchServiceV2Count(ctx *context.Context) {
|
||||
count, err := nuget_model.CountPackages(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: getSearchTerm(ctx),
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
go_context "context"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -19,36 +20,40 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
AppURL = "http://localhost:3000/"
|
||||
Repo = "gogits/gogs"
|
||||
FullURL = AppURL + Repo + "/"
|
||||
)
|
||||
const AppURL = "http://localhost:3000/"
|
||||
|
||||
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
|
||||
func testRenderMarkup(t *testing.T, mode string, wiki bool, filePath, text, expectedBody string, expectedCode int) {
|
||||
setting.AppURL = AppURL
|
||||
context := "/gogits/gogs"
|
||||
if !wiki {
|
||||
context += path.Join("/src/branch/main", path.Dir(filePath))
|
||||
}
|
||||
options := api.MarkupOption{
|
||||
Mode: mode,
|
||||
Text: text,
|
||||
Context: Repo,
|
||||
Wiki: true,
|
||||
Context: context,
|
||||
Wiki: wiki,
|
||||
FilePath: filePath,
|
||||
}
|
||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markup")
|
||||
web.SetForm(ctx, &options)
|
||||
Markup(ctx)
|
||||
assert.Equal(t, responseBody, resp.Body.String())
|
||||
assert.Equal(t, responseCode, resp.Code)
|
||||
assert.Equal(t, expectedBody, resp.Body.String())
|
||||
assert.Equal(t, expectedCode, resp.Code)
|
||||
resp.Body.Reset()
|
||||
}
|
||||
|
||||
func testRenderMarkdown(t *testing.T, mode, text, responseBody string, responseCode int) {
|
||||
func testRenderMarkdown(t *testing.T, mode string, wiki bool, text, responseBody string, responseCode int) {
|
||||
setting.AppURL = AppURL
|
||||
context := "/gogits/gogs"
|
||||
if !wiki {
|
||||
context += "/src/branch/main"
|
||||
}
|
||||
options := api.MarkdownOption{
|
||||
Mode: mode,
|
||||
Text: text,
|
||||
Context: Repo,
|
||||
Wiki: true,
|
||||
Context: context,
|
||||
Wiki: wiki,
|
||||
}
|
||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||
web.SetForm(ctx, &options)
|
||||
@@ -65,7 +70,7 @@ func TestAPI_RenderGFM(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
testCasesCommon := []string{
|
||||
testCasesWiki := []string{
|
||||
// dear imgui wiki markdown extract: special wiki syntax
|
||||
`Wiki! Enjoy :)
|
||||
- [[Links, Language bindings, Engine bindings|Links]]
|
||||
@@ -74,20 +79,20 @@ func TestAPI_RenderGFM(t *testing.T) {
|
||||
// rendered
|
||||
`<p>Wiki! Enjoy :)</p>
|
||||
<ul>
|
||||
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
|
||||
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||
<li><a href="http://localhost:3000/gogits/gogs/wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="http://localhost:3000/gogits/gogs/wiki/Tips" rel="nofollow">Tips</a></li>
|
||||
<li>Bezier widget (by <a href="http://localhost:3000/r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||
</ul>
|
||||
`,
|
||||
// Guard wiki sidebar: special syntax
|
||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||
// rendered
|
||||
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`,
|
||||
// special syntax
|
||||
`[[Name|Link]]`,
|
||||
// rendered
|
||||
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`<p><a href="http://localhost:3000/gogits/gogs/wiki/Link" rel="nofollow">Name</a></p>
|
||||
`,
|
||||
// empty
|
||||
``,
|
||||
@@ -95,7 +100,7 @@ func TestAPI_RenderGFM(t *testing.T) {
|
||||
``,
|
||||
}
|
||||
|
||||
testCasesDocument := []string{
|
||||
testCasesWikiDocument := []string{
|
||||
// wine-staging wiki home extract: special wiki syntax, images
|
||||
`## What is Wine Staging?
|
||||
**Wine Staging** on website [wine-staging.com](http://wine-staging.com).
|
||||
@@ -111,31 +116,48 @@ Here are some links to the most important topics. You can find the full list of
|
||||
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
||||
<h2 id="user-content-quick-links">Quick Links</h2>
|
||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||
<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
|
||||
<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||
<p><a href="http://localhost:3000/gogits/gogs/wiki/Configuration" rel="nofollow">Configuration</a>
|
||||
<a href="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" rel="nofollow"><img src="http://localhost:3000/gogits/gogs/wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||
`,
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCasesCommon); i += 2 {
|
||||
text := testCasesCommon[i]
|
||||
response := testCasesCommon[i+1]
|
||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
||||
testRenderMarkdown(t, "comment", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "comment", "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
|
||||
for i := 0; i < len(testCasesWiki); i += 2 {
|
||||
text := testCasesWiki[i]
|
||||
response := testCasesWiki[i+1]
|
||||
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
|
||||
testRenderMarkdown(t, "comment", true, text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "comment", true, "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
|
||||
}
|
||||
|
||||
for i := 0; i < len(testCasesDocument); i += 2 {
|
||||
text := testCasesDocument[i]
|
||||
response := testCasesDocument[i+1]
|
||||
testRenderMarkdown(t, "gfm", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", "path/test.md", text, response, http.StatusOK)
|
||||
for i := 0; i < len(testCasesWikiDocument); i += 2 {
|
||||
text := testCasesWikiDocument[i]
|
||||
response := testCasesWikiDocument[i+1]
|
||||
testRenderMarkdown(t, "gfm", true, text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "gfm", true, "", text, response, http.StatusOK)
|
||||
testRenderMarkup(t, "file", true, "path/test.md", text, response, http.StatusOK)
|
||||
}
|
||||
|
||||
testRenderMarkup(t, "file", "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
|
||||
testRenderMarkup(t, "unknown", "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
||||
input := "[Link](test.md)\n"
|
||||
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||
`, http.StatusOK)
|
||||
|
||||
testRenderMarkdown(t, "gfm", false, input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||
`, http.StatusOK)
|
||||
|
||||
testRenderMarkup(t, "gfm", false, "", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/test.md" rel="nofollow">Link</a>
|
||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/image.png" alt="Image"/></a></p>
|
||||
`, http.StatusOK)
|
||||
|
||||
testRenderMarkup(t, "file", false, "path/new-file.md", input, `<p><a href="http://localhost:3000/gogits/gogs/src/branch/main/path/test.md" rel="nofollow">Link</a>
|
||||
<a href="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" target="_blank" rel="nofollow noopener"><img src="http://localhost:3000/gogits/gogs/media/branch/main/path/image.png" alt="Image"/></a></p>
|
||||
`, http.StatusOK)
|
||||
|
||||
testRenderMarkup(t, "file", true, "path/test.unknown", "## Test", "Unsupported render extension: .unknown\n", http.StatusUnprocessableEntity)
|
||||
testRenderMarkup(t, "unknown", true, "", "## Test", "Unknown mode: unknown\n", http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
var simpleCases = []string{
|
||||
@@ -160,7 +182,7 @@ func TestAPI_RenderSimple(t *testing.T) {
|
||||
options := api.MarkdownOption{
|
||||
Mode: "markdown",
|
||||
Text: "",
|
||||
Context: Repo,
|
||||
Context: "/gogits/gogs",
|
||||
}
|
||||
ctx, resp := contexttest.MockAPIContext(t, "POST /api/v1/markdown")
|
||||
for i := 0; i < len(simpleCases); i += 2 {
|
||||
|
||||
@@ -319,6 +319,12 @@ func archiveDownload(ctx *context.APIContext) {
|
||||
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
|
||||
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
|
||||
|
||||
// Add nix format link header so tarballs lock correctly:
|
||||
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
||||
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
|
||||
ctx.Repo.Repository.APIURL(),
|
||||
archiver.CommitID, archiver.CommitID))
|
||||
|
||||
rPath := archiver.RelativePath()
|
||||
if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
|
||||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||||
|
||||
@@ -383,6 +383,7 @@ func CreatePushMirror(ctx *context.APIContext, mirrorOption *api.CreatePushMirro
|
||||
if err = mirror_service.AddPushMirrorRemote(ctx, pushMirror, address); err != nil {
|
||||
if err := repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{ID: pushMirror.ID, RepoID: pushMirror.RepoID}); err != nil {
|
||||
ctx.ServerError("DeletePushMirrors", err)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("AddPushMirrorRemote", err)
|
||||
return
|
||||
|
||||
+31
-30
@@ -7,62 +7,66 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
// RenderMarkup renders markup text for the /markup and /markdown endpoints
|
||||
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPrefix, filePath string, wiki bool) {
|
||||
var markupType string
|
||||
relativePath := ""
|
||||
func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPathContext, filePath string, wiki bool) {
|
||||
// urlPathContext format is "/subpath/{user}/{repo}/src/{branch, commit, tag}/{identifier/path}/{file/dir}"
|
||||
// filePath is the path of the file to render if the end user is trying to preview a repo file (mode == "file")
|
||||
// filePath will be used as RenderContext.RelativePath
|
||||
|
||||
if len(text) == 0 {
|
||||
_, _ = ctx.Write([]byte(""))
|
||||
return
|
||||
// for example, when previewing file "/gitea/owner/repo/src/branch/features/feat-123/doc/CHANGE.md", then filePath is "doc/CHANGE.md"
|
||||
// and the urlPathContext is "/gitea/owner/repo/src/branch/features/feat-123/doc"
|
||||
|
||||
var markupType, relativePath string
|
||||
|
||||
links := markup.Links{AbsolutePrefix: true}
|
||||
if urlPathContext != "" {
|
||||
links.Base = fmt.Sprintf("%s%s", httplib.GuessCurrentHostURL(ctx), urlPathContext)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case "markdown":
|
||||
// Raw markdown
|
||||
if err := markdown.RenderRaw(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: urlPrefix,
|
||||
},
|
||||
Ctx: ctx,
|
||||
Links: links,
|
||||
}, strings.NewReader(text), ctx.Resp); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
case "comment":
|
||||
// Comment as markdown
|
||||
// Issue & comment content
|
||||
markupType = markdown.MarkupName
|
||||
case "gfm":
|
||||
// Github Flavored Markdown as document
|
||||
// GitHub Flavored Markdown
|
||||
markupType = markdown.MarkupName
|
||||
case "file":
|
||||
// File as document based on file extension
|
||||
markupType = ""
|
||||
markupType = "" // render the repo file content by its extension
|
||||
relativePath = filePath
|
||||
default:
|
||||
ctx.Error(http.StatusUnprocessableEntity, fmt.Sprintf("Unknown mode: %s", mode))
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(setting.AppSubURL+"/", urlPrefix) {
|
||||
// check if urlPrefix is already set to a URL
|
||||
linkRegex, _ := xurls.StrictMatchingScheme("https?://")
|
||||
m := linkRegex.FindStringIndex(urlPrefix)
|
||||
if m == nil {
|
||||
urlPrefix = util.URLJoin(setting.AppURL, urlPrefix)
|
||||
}
|
||||
fields := strings.SplitN(strings.TrimPrefix(urlPathContext, setting.AppSubURL+"/"), "/", 5)
|
||||
if len(fields) == 5 && fields[2] == "src" && (fields[3] == "branch" || fields[3] == "commit" || fields[3] == "tag") {
|
||||
// absolute base prefix is something like "https://host/subpath/{user}/{repo}"
|
||||
absoluteBasePrefix := fmt.Sprintf("%s%s/%s", httplib.GuessCurrentAppURL(ctx), fields[0], fields[1])
|
||||
|
||||
fileDir := path.Dir(filePath) // it is "doc" if filePath is "doc/CHANGE.md"
|
||||
refPath := strings.Join(fields[3:], "/") // it is "branch/features/feat-12/doc"
|
||||
refPath = strings.TrimSuffix(refPath, "/"+fileDir) // now we get the correct branch path: "branch/features/feat-12"
|
||||
|
||||
links = markup.Links{AbsolutePrefix: true, Base: absoluteBasePrefix, BranchPath: refPath, TreePath: fileDir}
|
||||
}
|
||||
|
||||
meta := map[string]string{}
|
||||
@@ -78,11 +82,8 @@ func RenderMarkup(ctx *context.Base, repo *context.Repository, mode, text, urlPr
|
||||
}
|
||||
|
||||
if err := markup.Render(&markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
Links: markup.Links{
|
||||
AbsolutePrefix: true,
|
||||
Base: urlPrefix,
|
||||
},
|
||||
Ctx: ctx,
|
||||
Links: links,
|
||||
Metas: meta,
|
||||
IsWiki: wiki,
|
||||
Type: markupType,
|
||||
|
||||
@@ -95,6 +95,7 @@ func UnadoptedRepos(ctx *context.Context) {
|
||||
repoNames, count, err := repo_service.ListUnadoptedRepositories(ctx, q, &opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("ListUnadoptedRepositories", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["Dirs"] = repoNames
|
||||
pager := context.NewPagination(count, opts.PageSize, opts.Page, 5)
|
||||
|
||||
@@ -831,6 +831,7 @@ func ActivateEmail(ctx *context.Context) {
|
||||
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
|
||||
@@ -571,6 +571,7 @@ func MoveIssues(ctx *context.Context) {
|
||||
form := &movedIssuesForm{}
|
||||
if err = json.NewDecoder(ctx.Req.Body).Decode(&form); err != nil {
|
||||
ctx.ServerError("DecodeMovedIssuesForm", err)
|
||||
return
|
||||
}
|
||||
|
||||
issueIDs := make([]int64, 0, len(form.Issues))
|
||||
|
||||
@@ -19,14 +19,8 @@ const (
|
||||
// Contributors render the page to show repository contributors graph
|
||||
func Contributors(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors")
|
||||
|
||||
ctx.Data["PageIsActivity"] = true
|
||||
ctx.Data["PageIsContributors"] = true
|
||||
|
||||
ctx.PageData["contributionType"] = "commits"
|
||||
|
||||
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
|
||||
|
||||
ctx.HTML(http.StatusOK, tplContributors)
|
||||
}
|
||||
|
||||
|
||||
@@ -562,6 +562,7 @@ func DeleteFilePost(ctx *context.Context) {
|
||||
} else {
|
||||
ctx.ServerError("DeleteRepoFile", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
||||
|
||||
@@ -1791,6 +1791,7 @@ func ViewIssue(ctx *context.Context) {
|
||||
pull.Issue = issue
|
||||
canDelete := false
|
||||
allowMerge := false
|
||||
canWriteToHeadRepo := false
|
||||
|
||||
if ctx.IsSigned {
|
||||
if err := pull.LoadHeadRepo(ctx); err != nil {
|
||||
@@ -1811,7 +1812,7 @@ func ViewIssue(ctx *context.Context) {
|
||||
ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
|
||||
}
|
||||
}
|
||||
ctx.Data["CanWriteToHeadRepo"] = true
|
||||
canWriteToHeadRepo = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1823,6 +1824,9 @@ func ViewIssue(ctx *context.Context) {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
|
||||
canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode)
|
||||
}
|
||||
allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("IsUserAllowedToMerge", err)
|
||||
@@ -1835,6 +1839,8 @@ func ViewIssue(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
|
||||
ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
|
||||
ctx.Data["AllowMerge"] = allowMerge
|
||||
|
||||
prUnit, err := repo.GetUnit(ctx, unit.TypePullRequests)
|
||||
@@ -1889,13 +1895,9 @@ func ViewIssue(ctx *context.Context) {
|
||||
ctx.ServerError("LoadProtectedBranch", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["ShowMergeInstructions"] = true
|
||||
|
||||
if pb != nil {
|
||||
pb.Repo = pull.BaseRepo
|
||||
var showMergeInstructions bool
|
||||
if ctx.Doer != nil {
|
||||
showMergeInstructions = pb.CanUserPush(ctx, ctx.Doer)
|
||||
}
|
||||
ctx.Data["ProtectedBranch"] = pb
|
||||
ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
|
||||
ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
|
||||
@@ -1906,7 +1908,6 @@ func ViewIssue(ctx *context.Context) {
|
||||
ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
|
||||
ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
|
||||
ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
|
||||
ctx.Data["ShowMergeInstructions"] = showMergeInstructions
|
||||
}
|
||||
ctx.Data["WillSign"] = false
|
||||
if ctx.Doer != nil {
|
||||
|
||||
@@ -47,7 +47,7 @@ func RenderFile(ctx *context.Context) {
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
ctx.Resp.Header().Add("Content-Security-Policy", "frame-src 'self'; sandbox allow-scripts")
|
||||
|
||||
if markupType := markup.Type(blob.Name()); markupType == "" {
|
||||
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType == "" {
|
||||
if isTextFile {
|
||||
_, _ = io.Copy(ctx.Resp, rd)
|
||||
} else {
|
||||
|
||||
@@ -418,8 +418,9 @@ func RedirectDownload(ctx *context.Context) {
|
||||
tagNames := []string{vTag}
|
||||
curRepo := ctx.Repo.Repository
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: curRepo.ID,
|
||||
TagNames: tagNames,
|
||||
IncludeDrafts: ctx.Repo.CanWrite(unit.TypeReleases),
|
||||
RepoID: curRepo.ID,
|
||||
TagNames: tagNames,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ServerError("RedirectDownload", err)
|
||||
@@ -484,6 +485,12 @@ func Download(ctx *context.Context) {
|
||||
func download(ctx *context.Context, archiveName string, archiver *repo_model.RepoArchiver) {
|
||||
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
|
||||
|
||||
// Add nix format link header so tarballs lock correctly:
|
||||
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
||||
ctx.Resp.Header().Add("Link", fmt.Sprintf(`<%s/archive/%s.tar.gz?rev=%s>; rel="immutable"`,
|
||||
ctx.Repo.Repository.APIURL(),
|
||||
archiver.CommitID, archiver.CommitID))
|
||||
|
||||
rPath := archiver.RelativePath()
|
||||
if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
|
||||
// If we have a signed url (S3, object storage), redirect to this directly.
|
||||
|
||||
@@ -307,7 +307,7 @@ func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.Tr
|
||||
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc), charset.ConvertOpts{})
|
||||
|
||||
if markupType := markup.Type(readmeFile.Name()); markupType != "" {
|
||||
if markupType := markup.DetectMarkupTypeByFileName(readmeFile.Name()); markupType != "" {
|
||||
ctx.Data["IsMarkup"] = true
|
||||
ctx.Data["MarkupType"] = markupType
|
||||
|
||||
@@ -499,7 +499,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
readmeExist := util.IsReadmeFileName(blob.Name())
|
||||
ctx.Data["ReadmeExist"] = readmeExist
|
||||
|
||||
markupType := markup.Type(blob.Name())
|
||||
markupType := markup.DetectMarkupTypeByFileName(blob.Name())
|
||||
// If the markup is detected by custom markup renderer it should not be reset later on
|
||||
// to not pass it down to the render context.
|
||||
detected := false
|
||||
@@ -606,9 +606,9 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: this logic seems strange, it duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
||||
// maybe for this case, the file is a binary file, and shouldn't be rendered?
|
||||
if markupType := markup.Type(blob.Name()); markupType != "" {
|
||||
// TODO: this logic duplicates with "isRepresentableAsText=true", it is not the same as "LFSFileGet" in "lfs.go"
|
||||
// It is used by "external renders", markupRender will execute external programs to get rendered content.
|
||||
if markupType := markup.DetectMarkupTypeByFileName(blob.Name()); markupType != "" {
|
||||
rd := io.MultiReader(bytes.NewReader(buf), dataRc)
|
||||
ctx.Data["IsMarkup"] = true
|
||||
ctx.Data["MarkupType"] = markupType
|
||||
@@ -1047,8 +1047,7 @@ func renderHomeCode(ctx *context.Context) {
|
||||
baseRepoPerm.CanRead(unit_model.TypePullRequests) {
|
||||
ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts)
|
||||
if err != nil {
|
||||
ctx.ServerError("FindRecentlyPushedNewBranches", err)
|
||||
return
|
||||
log.Error("FindRecentlyPushedNewBranches failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@ func Wiki(ctx *context.Context) {
|
||||
}
|
||||
|
||||
wikiPath := entry.Name()
|
||||
if markup.Type(wikiPath) != markdown.MarkupName {
|
||||
if markup.DetectMarkupTypeByFileName(wikiPath) != markdown.MarkupName {
|
||||
ext := strings.ToUpper(filepath.Ext(wikiPath))
|
||||
ctx.Data["FormatWarning"] = fmt.Sprintf("%s rendering is not supported at the moment. Rendered as Markdown.", ext)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
@@ -15,6 +16,7 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||
@@ -178,7 +180,11 @@ func ViewPackageVersion(ctx *context.Context) {
|
||||
|
||||
switch pd.Package.Type {
|
||||
case packages_model.TypeContainer:
|
||||
ctx.Data["RegistryHost"] = setting.Packages.RegistryHost
|
||||
registryAppURL, err := url.Parse(httplib.GuessCurrentAppURL(ctx))
|
||||
if err != nil {
|
||||
registryAppURL, _ = url.Parse(setting.AppURL)
|
||||
}
|
||||
ctx.Data["RegistryHost"] = registryAppURL.Host
|
||||
case packages_model.TypeAlpine:
|
||||
branches := make(container.Set[string])
|
||||
repositories := make(container.Set[string])
|
||||
|
||||
+3
-2
@@ -1125,6 +1125,9 @@ func registerRoutes(m *web.Route) {
|
||||
// user/org home, including rss feeds
|
||||
m.Get("/{username}/{reponame}", ignSignIn, context.RepoAssignment, context.RepoRef(), repo.SetEditorconfigIfExists, repo.Home)
|
||||
|
||||
// TODO: maybe it should relax the permission to allow "any access"
|
||||
m.Post("/{username}/{reponame}/markup", ignSignIn, context.RepoAssignment, context.RequireRepoReaderOr(unit.TypeCode, unit.TypeIssues, unit.TypePullRequests, unit.TypeReleases, unit.TypeWiki), web.Bind(structs.MarkupOption{}), misc.Markup)
|
||||
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Get("/find/*", repo.FindFiles)
|
||||
m.Group("/tree-list", func() {
|
||||
@@ -1236,8 +1239,6 @@ func registerRoutes(m *web.Route) {
|
||||
m.Post("/reactions/{action}", web.Bind(forms.ReactionForm{}), repo.ChangeCommentReaction)
|
||||
}, context.RepoMustNotBeArchived())
|
||||
|
||||
m.Post("/markup", web.Bind(structs.MarkupOption{}), misc.Markup)
|
||||
|
||||
m.Group("/labels", func() {
|
||||
m.Post("/new", web.Bind(forms.CreateLabelForm{}), repo.NewLabel)
|
||||
m.Post("/edit", web.Bind(forms.CreateLabelForm{}), repo.UpdateLabel)
|
||||
|
||||
@@ -236,6 +236,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, permissionInR
|
||||
MirrorInterval: mirrorInterval,
|
||||
MirrorUpdated: mirrorUpdated,
|
||||
RepoTransfer: transfer,
|
||||
ObjectFormatName: repo.ObjectFormatName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package externalaccount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
@@ -82,6 +83,11 @@ func UpdateExternalUser(ctx context.Context, user *user_model.User, gothUser got
|
||||
|
||||
// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID
|
||||
func UpdateMigrationsByType(ctx context.Context, tp structs.GitServiceType, externalUserID string, userID int64) error {
|
||||
// Skip update if externalUserID is not a valid numeric ID or exceeds int64
|
||||
if _, err := strconv.ParseInt(externalUserID, 10, 64); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := issues_model.UpdateIssuesMigrationsByType(ctx, tp, externalUserID, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user