2020-01-07 21:27:36 +03:00
// Copyright 2015 The Gogs Authors. All rights reserved.
// Copyright 2019 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2020-01-07 21:27:36 +03:00
package wiki
import (
2022-01-20 02:26:57 +03:00
"context"
2020-01-07 21:27:36 +03:00
"fmt"
"net/url"
"os"
"strings"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2022-10-17 02:29:26 +03:00
system_model "code.gitea.io/gitea/models/system"
2021-11-09 22:57:58 +03:00
"code.gitea.io/gitea/models/unit"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2020-01-07 21:27:36 +03:00
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
2020-01-20 23:01:19 +03:00
repo_module "code.gitea.io/gitea/modules/repository"
2020-01-07 21:27:36 +03:00
"code.gitea.io/gitea/modules/sync"
"code.gitea.io/gitea/modules/util"
2021-12-10 11:14:24 +03:00
asymkey_service "code.gitea.io/gitea/services/asymkey"
2020-01-07 21:27:36 +03:00
)
var (
reservedWikiNames = [ ] string { "_pages" , "_new" , "_edit" , "raw" }
2022-05-04 19:06:23 +03:00
// TODO: use clustered lock (unique queue? or *abuse* cache)
wikiWorkingPool = sync . NewExclusivePool ( )
2020-01-07 21:27:36 +03:00
)
2022-10-15 17:40:32 +03:00
const (
DefaultRemote = "origin"
DefaultBranch = "master"
)
2020-01-07 21:27:36 +03:00
func nameAllowed ( name string ) error {
Improve utils of slices (#22379)
- Move the file `compare.go` and `slice.go` to `slice.go`.
- Fix `ExistsInSlice`, it's buggy
- It uses `sort.Search`, so it assumes that the input slice is sorted.
- It passes `func(i int) bool { return slice[i] == target })` to
`sort.Search`, that's incorrect, check the doc of `sort.Search`.
- Conbine `IsInt64InSlice(int64, []int64)` and `ExistsInSlice(string,
[]string)` to `SliceContains[T]([]T, T)`.
- Conbine `IsSliceInt64Eq([]int64, []int64)` and `IsEqualSlice([]string,
[]string)` to `SliceSortedEqual[T]([]T, T)`.
- Add `SliceEqual[T]([]T, T)` as a distinction from
`SliceSortedEqual[T]([]T, T)`.
- Redesign `RemoveIDFromList([]int64, int64) ([]int64, bool)` to
`SliceRemoveAll[T]([]T, T) []T`.
- Add `SliceContainsFunc[T]([]T, func(T) bool)` and
`SliceRemoveAllFunc[T]([]T, func(T) bool)` for general use.
- Add comments to explain why not `golang.org/x/exp/slices`.
- Add unit tests.
2023-01-11 08:31:16 +03:00
if util . SliceContainsString ( reservedWikiNames , name ) {
2022-08-25 05:31:57 +03:00
return repo_model . ErrWikiReservedName {
2020-01-07 21:27:36 +03:00
Title : name ,
}
}
return nil
}
// NameToSubURL converts a wiki name to its corresponding sub-URL.
func NameToSubURL ( name string ) string {
2021-11-16 21:18:25 +03:00
return url . PathEscape ( strings . ReplaceAll ( name , " " , "-" ) )
2020-01-07 21:27:36 +03:00
}
// NormalizeWikiName normalizes a wiki name
func NormalizeWikiName ( name string ) string {
2020-10-11 23:27:20 +03:00
return strings . ReplaceAll ( name , "-" , " " )
2020-01-07 21:27:36 +03:00
}
// NameToFilename converts a wiki name to its corresponding filename.
func NameToFilename ( name string ) string {
2020-10-11 23:27:20 +03:00
name = strings . ReplaceAll ( name , " " , "-" )
2020-01-07 21:27:36 +03:00
return url . QueryEscape ( name ) + ".md"
}
// FilenameToName converts a wiki filename to its corresponding page name.
func FilenameToName ( filename string ) ( string , error ) {
if ! strings . HasSuffix ( filename , ".md" ) {
2022-08-25 05:31:57 +03:00
return "" , repo_model . ErrWikiInvalidFileName {
2020-01-07 21:27:36 +03:00
FileName : filename ,
}
}
basename := filename [ : len ( filename ) - 3 ]
unescaped , err := url . QueryUnescape ( basename )
if err != nil {
return "" , err
}
return NormalizeWikiName ( unescaped ) , nil
}
// InitWiki initializes a wiki for repository,
// it does nothing when repository already has wiki.
2022-01-20 02:26:57 +03:00
func InitWiki ( ctx context . Context , repo * repo_model . Repository ) error {
2020-01-07 21:27:36 +03:00
if repo . HasWiki ( ) {
return nil
}
2022-01-20 02:26:57 +03:00
if err := git . InitRepository ( ctx , repo . WikiPath ( ) , true ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "InitRepository: %w" , err )
2020-01-20 23:01:19 +03:00
} else if err = repo_module . CreateDelegateHooks ( repo . WikiPath ( ) ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "createDelegateHooks: %w" , err )
2022-10-15 17:40:32 +03:00
} else if _ , _ , err = git . NewCommand ( ctx , "symbolic-ref" , "HEAD" , git . BranchPrefix + DefaultBranch ) . RunStdString ( & git . RunOpts { Dir : repo . WikiPath ( ) } ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "unable to set default wiki branch to master: %w" , err )
2020-01-07 21:27:36 +03:00
}
return nil
}
2021-07-08 02:23:09 +03:00
// prepareWikiFileName try to find a suitable file path with file name by the given raw wiki name.
// return: existence, prepared file path with name, error
func prepareWikiFileName ( gitRepo * git . Repository , wikiName string ) ( bool , string , error ) {
unescaped := wikiName + ".md"
escaped := NameToFilename ( wikiName )
// Look for both files
2022-10-15 17:40:32 +03:00
filesInIndex , err := gitRepo . LsTree ( DefaultBranch , unescaped , escaped )
2021-07-08 02:23:09 +03:00
if err != nil {
2021-08-01 20:04:32 +03:00
if strings . Contains ( err . Error ( ) , "Not a valid object name master" ) {
return false , escaped , nil
}
2021-07-08 02:23:09 +03:00
log . Error ( "%v" , err )
return false , escaped , err
}
foundEscaped := false
for _ , filename := range filesInIndex {
switch filename {
case unescaped :
// if we find the unescaped file return it
return true , unescaped , nil
case escaped :
foundEscaped = true
}
}
// If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
return foundEscaped , escaped , nil
}
2022-09-04 22:54:23 +03:00
// updateWikiPage adds a new page or edits an existing page in repository wiki.
2022-01-20 02:26:57 +03:00
func updateWikiPage ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , oldWikiName , newWikiName , content , message string , isNew bool ) ( err error ) {
2020-01-07 21:27:36 +03:00
if err = nameAllowed ( newWikiName ) ; err != nil {
return err
}
2020-12-25 12:59:32 +03:00
wikiWorkingPool . CheckIn ( fmt . Sprint ( repo . ID ) )
defer wikiWorkingPool . CheckOut ( fmt . Sprint ( repo . ID ) )
2020-01-07 21:27:36 +03:00
2022-01-20 02:26:57 +03:00
if err = InitWiki ( ctx , repo ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "InitWiki: %w" , err )
2020-01-07 21:27:36 +03:00
}
2022-10-15 17:40:32 +03:00
hasMasterBranch := git . IsBranchExist ( ctx , repo . WikiPath ( ) , DefaultBranch )
2020-01-07 21:27:36 +03:00
2022-05-08 19:46:32 +03:00
basePath , err := repo_module . CreateTemporaryPath ( "update-wiki" )
2020-01-07 21:27:36 +03:00
if err != nil {
return err
}
defer func ( ) {
2022-05-08 19:46:32 +03:00
if err := repo_module . RemoveTemporaryPath ( basePath ) ; err != nil {
2020-01-07 21:27:36 +03:00
log . Error ( "Merge: RemoveTemporaryPath: %s" , err )
}
} ( )
cloneOpts := git . CloneRepoOptions {
Bare : true ,
Shared : true ,
}
if hasMasterBranch {
2022-10-15 17:40:32 +03:00
cloneOpts . Branch = DefaultBranch
2020-01-07 21:27:36 +03:00
}
2022-01-20 02:26:57 +03:00
if err := git . Clone ( ctx , repo . WikiPath ( ) , basePath , cloneOpts ) ; err != nil {
2020-01-07 21:27:36 +03:00
log . Error ( "Failed to clone repository: %s (%v)" , repo . FullName ( ) , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Failed to clone repository: %s (%w)" , repo . FullName ( ) , err )
2020-01-07 21:27:36 +03:00
}
2022-03-29 22:13:41 +03:00
gitRepo , err := git . OpenRepository ( ctx , basePath )
2020-01-07 21:27:36 +03:00
if err != nil {
log . Error ( "Unable to open temporary repository: %s (%v)" , basePath , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Failed to open new temporary repository in: %s %w" , basePath , err )
2020-01-07 21:27:36 +03:00
}
defer gitRepo . Close ( )
if hasMasterBranch {
if err := gitRepo . ReadTreeToIndex ( "HEAD" ) ; err != nil {
log . Error ( "Unable to read HEAD tree to index in: %s %v" , basePath , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Unable to read HEAD tree to index in: %s %w" , basePath , err )
2020-01-07 21:27:36 +03:00
}
}
2021-07-08 02:23:09 +03:00
isWikiExist , newWikiPath , err := prepareWikiFileName ( gitRepo , newWikiName )
if err != nil {
return err
}
2020-01-07 21:27:36 +03:00
if isNew {
2021-07-08 02:23:09 +03:00
if isWikiExist {
2022-08-25 05:31:57 +03:00
return repo_model . ErrWikiAlreadyExist {
2020-01-07 21:27:36 +03:00
Title : newWikiPath ,
}
}
} else {
2021-07-08 02:23:09 +03:00
// avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free.
isOldWikiExist := true
oldWikiPath := newWikiPath
if oldWikiName != newWikiName {
isOldWikiExist , oldWikiPath , err = prepareWikiFileName ( gitRepo , oldWikiName )
if err != nil {
return err
}
2020-01-07 21:27:36 +03:00
}
2021-07-08 02:23:09 +03:00
if isOldWikiExist {
2020-01-07 21:27:36 +03:00
err := gitRepo . RemoveFilesFromIndex ( oldWikiPath )
if err != nil {
log . Error ( "%v" , err )
return err
}
}
}
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
objectHash , err := gitRepo . HashObject ( strings . NewReader ( content ) )
if err != nil {
log . Error ( "%v" , err )
return err
}
if err := gitRepo . AddObjectToIndex ( "100644" , objectHash , newWikiPath ) ; err != nil {
log . Error ( "%v" , err )
return err
}
tree , err := gitRepo . WriteTree ( )
if err != nil {
log . Error ( "%v" , err )
return err
}
commitTreeOpts := git . CommitTreeOpts {
Message : message ,
}
2020-09-19 19:44:55 +03:00
committer := doer . NewGitSig ( )
2022-01-20 02:26:57 +03:00
sign , signingKey , signer , _ := asymkey_service . SignWikiCommit ( ctx , repo . WikiPath ( ) , doer )
2020-01-07 21:27:36 +03:00
if sign {
commitTreeOpts . KeyID = signingKey
2021-12-10 04:27:50 +03:00
if repo . GetTrustModel ( ) == repo_model . CommitterTrustModel || repo . GetTrustModel ( ) == repo_model . CollaboratorCommitterTrustModel {
2020-09-19 19:44:55 +03:00
committer = signer
}
2020-01-07 21:27:36 +03:00
} else {
commitTreeOpts . NoGPGSign = true
}
if hasMasterBranch {
commitTreeOpts . Parents = [ ] string { "HEAD" }
}
2020-09-19 19:44:55 +03:00
commitHash , err := gitRepo . CommitTree ( doer . NewGitSig ( ) , committer , tree , commitTreeOpts )
2020-01-07 21:27:36 +03:00
if err != nil {
log . Error ( "%v" , err )
return err
}
2021-11-30 23:06:32 +03:00
if err := git . Push ( gitRepo . Ctx , basePath , git . PushOptions {
2022-10-15 17:40:32 +03:00
Remote : DefaultRemote ,
Branch : fmt . Sprintf ( "%s:%s%s" , commitHash . String ( ) , git . BranchPrefix , DefaultBranch ) ,
2022-05-08 19:46:32 +03:00
Env : repo_module . FullPushingEnvironment (
2020-01-07 21:27:36 +03:00
doer ,
doer ,
repo ,
repo . Name + ".wiki" ,
0 ,
) ,
} ) ; err != nil {
log . Error ( "%v" , err )
2020-03-28 07:13:18 +03:00
if git . IsErrPushOutOfDate ( err ) || git . IsErrPushRejected ( err ) {
return err
}
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Push: %w" , err )
2020-01-07 21:27:36 +03:00
}
return nil
}
// AddWikiPage adds a new wiki page with a given wikiPath.
2022-01-20 02:26:57 +03:00
func AddWikiPage ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , wikiName , content , message string ) error {
return updateWikiPage ( ctx , doer , repo , "" , wikiName , content , message , true )
2020-01-07 21:27:36 +03:00
}
// EditWikiPage updates a wiki page identified by its wikiPath,
// optionally also changing wikiPath.
2022-01-20 02:26:57 +03:00
func EditWikiPage ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , oldWikiName , newWikiName , content , message string ) error {
return updateWikiPage ( ctx , doer , repo , oldWikiName , newWikiName , content , message , false )
2020-01-07 21:27:36 +03:00
}
// DeleteWikiPage deletes a wiki page identified by its path.
2022-01-20 02:26:57 +03:00
func DeleteWikiPage ( ctx context . Context , doer * user_model . User , repo * repo_model . Repository , wikiName string ) ( err error ) {
2020-12-25 12:59:32 +03:00
wikiWorkingPool . CheckIn ( fmt . Sprint ( repo . ID ) )
defer wikiWorkingPool . CheckOut ( fmt . Sprint ( repo . ID ) )
2020-01-07 21:27:36 +03:00
2022-01-20 02:26:57 +03:00
if err = InitWiki ( ctx , repo ) ; err != nil {
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "InitWiki: %w" , err )
2020-01-07 21:27:36 +03:00
}
2022-05-08 19:46:32 +03:00
basePath , err := repo_module . CreateTemporaryPath ( "update-wiki" )
2020-01-07 21:27:36 +03:00
if err != nil {
return err
}
defer func ( ) {
2022-05-08 19:46:32 +03:00
if err := repo_module . RemoveTemporaryPath ( basePath ) ; err != nil {
2020-01-07 21:27:36 +03:00
log . Error ( "Merge: RemoveTemporaryPath: %s" , err )
}
} ( )
2022-01-20 02:26:57 +03:00
if err := git . Clone ( ctx , repo . WikiPath ( ) , basePath , git . CloneRepoOptions {
2020-01-07 21:27:36 +03:00
Bare : true ,
Shared : true ,
2022-10-15 17:40:32 +03:00
Branch : DefaultBranch ,
2020-01-07 21:27:36 +03:00
} ) ; err != nil {
log . Error ( "Failed to clone repository: %s (%v)" , repo . FullName ( ) , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Failed to clone repository: %s (%w)" , repo . FullName ( ) , err )
2020-01-07 21:27:36 +03:00
}
2022-03-29 22:13:41 +03:00
gitRepo , err := git . OpenRepository ( ctx , basePath )
2020-01-07 21:27:36 +03:00
if err != nil {
log . Error ( "Unable to open temporary repository: %s (%v)" , basePath , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Failed to open new temporary repository in: %s %w" , basePath , err )
2020-01-07 21:27:36 +03:00
}
defer gitRepo . Close ( )
if err := gitRepo . ReadTreeToIndex ( "HEAD" ) ; err != nil {
log . Error ( "Unable to read HEAD tree to index in: %s %v" , basePath , err )
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Unable to read HEAD tree to index in: %s %w" , basePath , err )
2020-01-07 21:27:36 +03:00
}
2021-07-19 19:14:00 +03:00
found , wikiPath , err := prepareWikiFileName ( gitRepo , wikiName )
if err != nil {
return err
2020-01-07 21:27:36 +03:00
}
if found {
err := gitRepo . RemoveFilesFromIndex ( wikiPath )
if err != nil {
return err
}
} else {
return os . ErrNotExist
}
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
tree , err := gitRepo . WriteTree ( )
if err != nil {
return err
}
message := "Delete page '" + wikiName + "'"
commitTreeOpts := git . CommitTreeOpts {
Message : message ,
Parents : [ ] string { "HEAD" } ,
}
2020-09-19 19:44:55 +03:00
committer := doer . NewGitSig ( )
2022-01-20 02:26:57 +03:00
sign , signingKey , signer , _ := asymkey_service . SignWikiCommit ( ctx , repo . WikiPath ( ) , doer )
2020-01-07 21:27:36 +03:00
if sign {
commitTreeOpts . KeyID = signingKey
2021-12-10 04:27:50 +03:00
if repo . GetTrustModel ( ) == repo_model . CommitterTrustModel || repo . GetTrustModel ( ) == repo_model . CollaboratorCommitterTrustModel {
2020-09-19 19:44:55 +03:00
committer = signer
}
2020-01-07 21:27:36 +03:00
} else {
commitTreeOpts . NoGPGSign = true
}
2020-09-19 19:44:55 +03:00
commitHash , err := gitRepo . CommitTree ( doer . NewGitSig ( ) , committer , tree , commitTreeOpts )
2020-01-07 21:27:36 +03:00
if err != nil {
return err
}
2021-11-30 23:06:32 +03:00
if err := git . Push ( gitRepo . Ctx , basePath , git . PushOptions {
2022-10-15 17:40:32 +03:00
Remote : DefaultRemote ,
Branch : fmt . Sprintf ( "%s:%s%s" , commitHash . String ( ) , git . BranchPrefix , DefaultBranch ) ,
2022-05-08 19:46:32 +03:00
Env : repo_module . PushingEnvironment ( doer , repo ) ,
2020-01-07 21:27:36 +03:00
} ) ; err != nil {
2020-03-28 07:13:18 +03:00
if git . IsErrPushOutOfDate ( err ) || git . IsErrPushRejected ( err ) {
return err
}
2022-10-24 22:29:17 +03:00
return fmt . Errorf ( "Push: %w" , err )
2020-01-07 21:27:36 +03:00
}
return nil
}
2021-09-08 18:19:30 +03:00
// DeleteWiki removes the actual and local copy of repository wiki.
2022-01-20 02:26:57 +03:00
func DeleteWiki ( ctx context . Context , repo * repo_model . Repository ) error {
2021-12-12 18:48:20 +03:00
if err := repo_model . UpdateRepositoryUnits ( repo , nil , [ ] unit . Type { unit . TypeWiki } ) ; err != nil {
2021-09-08 18:19:30 +03:00
return err
}
2022-10-17 02:29:26 +03:00
system_model . RemoveAllWithNotice ( ctx , "Delete repository wiki" , repo . WikiPath ( ) )
2021-09-08 18:19:30 +03:00
return nil
}