diff --git a/CHANGELOG.md b/CHANGELOG.md index 373ce00e6..b31730a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ This changelog goes through all 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.io). +## [1.4.3](https://github.com/go-gitea/gitea/releases/tag/v1.4.3) - 2018-06-26 +* SECURITY + * HTML-escape plain-text READMEs (#4192) (#4214) + * Fix open redirect vulnerability on login screen (#4312) (#4312) +* BUGFIXES + * Fix broken monitoring page when running processes are shown (#4203) (#4208) + * Fix delete comment bug (#4216) (#4228) + * Delete reactions added to issues and comments when deleting repository (#4232) (#4237) + * Fix wiki URL encoding bug (#4091) (#4254) + * Fix code tab link when viewing tags (#3908) (#4263) + * Fix webhook type conflation (#4285) (#4285) + ## [1.4.2](https://github.com/go-gitea/gitea/releases/tag/v1.4.2) - 2018-06-04 * BUGFIXES * Adjust z-index for floating labels (#3939) (#3950) diff --git a/models/migrations/v68.go b/models/migrations/v68.go index d6a0d04c5..e27b896c8 100644 --- a/models/migrations/v68.go +++ b/models/migrations/v68.go @@ -5,19 +5,47 @@ package migrations import ( + "fmt" + "regexp" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" "github.com/go-xorm/xorm" ) +var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) + +func validateTopic(topic string) bool { + return len(topic) <= 35 && topicPattern.MatchString(topic) +} + func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { log.Info("This migration could take up to minutes, please be patient.") + type Topic struct { - ID int64 - Name string `xorm:"unique"` + ID int64 + Name string `xorm:"UNIQUE"` + RepoCount int + CreatedUnix int64 `xorm:"INDEX created"` + UpdatedUnix int64 `xorm:"INDEX updated"` + } + + type RepoTopic struct { + RepoID int64 `xorm:"UNIQUE(s)"` + TopicID int64 `xorm:"UNIQUE(s)"` + } + + type Repository struct { + ID int64 `xorm:"pk autoincr"` + Topics []string `xorm:"TEXT JSON"` + } + + if err := x.Sync2(new(Topic)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + if err := x.Sync2(new(RepoTopic)); err != nil { + return fmt.Errorf("Sync2: %v", err) } sess := x.NewSession() @@ -25,79 +53,99 @@ func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { const batchSize = 100 touchedRepo := make(map[int64]struct{}) - topics := make([]*Topic, 0, batchSize) delTopicIDs := make([]int64, 0, batchSize) - ids := make([]int64, 0, 30) + log.Info("Validating existed topics...") if err := sess.Begin(); err != nil { return err } - log.Info("Validating existed topics...") for start := 0; ; start += batchSize { - topics = topics[:0] - if err := sess.Asc("id").Limit(batchSize, start).Find(&topics); err != nil { + topics := make([]*Topic, 0, batchSize) + if err := x.Cols("id", "name").Asc("id").Limit(batchSize, start).Find(&topics); err != nil { return err } if len(topics) == 0 { break } for _, topic := range topics { - if models.ValidateTopic(topic.Name) { + if validateTopic(topic.Name) { continue } + log.Info("Incorrect topic: id = %v, name = %q", topic.ID, topic.Name) + topic.Name = strings.Replace(strings.TrimSpace(strings.ToLower(topic.Name)), " ", "-", -1) + ids := make([]int64, 0, 30) if err := sess.Table("repo_topic").Cols("repo_id"). Where("topic_id = ?", topic.ID).Find(&ids); err != nil { return err } + log.Info("Touched repo ids: %v", ids) for _, id := range ids { touchedRepo[id] = struct{}{} } - if models.ValidateTopic(topic.Name) { - log.Info("Updating topic: id = %v, name = %v", topic.ID, topic.Name) - if _, err := sess.Table("topic").ID(topic.ID). - Update(&Topic{Name: topic.Name}); err != nil { + if validateTopic(topic.Name) { + unifiedTopic := Topic{Name: topic.Name} + exists, err := sess.Cols("id", "name").Get(&unifiedTopic) + log.Info("Exists topic with the name %q? %v, id = %v", topic.Name, exists, unifiedTopic.ID) + if err != nil { return err } - } else { - delTopicIDs = append(delTopicIDs, topic.ID) + if exists { + log.Info("Updating repo_topic rows with topic_id = %v to topic_id = %v", topic.ID, unifiedTopic.ID) + if _, err := sess.Where("topic_id = ? AND repo_id NOT IN "+ + "(SELECT rt1.repo_id FROM repo_topic rt1 INNER JOIN repo_topic rt2 "+ + "ON rt1.repo_id = rt2.repo_id WHERE rt1.topic_id = ? AND rt2.topic_id = ?)", + topic.ID, topic.ID, unifiedTopic.ID).Update(&RepoTopic{TopicID: unifiedTopic.ID}); err != nil { + return err + } + log.Info("Updating topic `repo_count` field") + if _, err := sess.Exec( + "UPDATE topic SET repo_count = (SELECT COUNT(*) FROM repo_topic WHERE topic_id = ? GROUP BY topic_id) WHERE id = ?", + unifiedTopic.ID, unifiedTopic.ID); err != nil { + return err + } + } else { + log.Info("Updating topic: id = %v, name = %q", topic.ID, topic.Name) + if _, err := sess.Table("topic").ID(topic.ID). + Update(&Topic{Name: topic.Name}); err != nil { + return err + } + continue + } } + delTopicIDs = append(delTopicIDs, topic.ID) } } + if err := sess.Commit(); err != nil { + return err + } + + sess.Init() log.Info("Deleting incorrect topics...") - for start := 0; ; start += batchSize { - if (start + batchSize) < len(delTopicIDs) { - ids = delTopicIDs[start:(start + batchSize)] - } else { - ids = delTopicIDs[start:] - } - - log.Info("Deleting 'repo_topic' rows for topics with ids = %v", ids) - if _, err := sess.In("topic_id", ids).Delete(&models.RepoTopic{}); err != nil { - return err - } - - log.Info("Deleting topics with id = %v", ids) - if _, err := sess.In("id", ids).Delete(&Topic{}); err != nil { - return err - } - - if len(ids) < batchSize { - break - } + if err := sess.Begin(); err != nil { + return err + } + log.Info("Deleting 'repo_topic' rows for topics with ids = %v", delTopicIDs) + if _, err := sess.In("topic_id", delTopicIDs).Delete(&RepoTopic{}); err != nil { + return err + } + log.Info("Deleting topics with id = %v", delTopicIDs) + if _, err := sess.In("id", delTopicIDs).Delete(&Topic{}); err != nil { + return err + } + if err := sess.Commit(); err != nil { + return err } - repoTopics := make([]*models.RepoTopic, 0, batchSize) - delRepoTopics := make([]*models.RepoTopic, 0, batchSize) - tmpRepoTopics := make([]*models.RepoTopic, 0, 30) + delRepoTopics := make([]*RepoTopic, 0, batchSize) log.Info("Checking the number of topics in the repositories...") for start := 0; ; start += batchSize { - repoTopics = repoTopics[:0] - if err := sess.Cols("repo_id").Asc("repo_id").Limit(batchSize, start). + repoTopics := make([]*RepoTopic, 0, batchSize) + if err := x.Cols("repo_id").Asc("repo_id").Limit(batchSize, start). GroupBy("repo_id").Having("COUNT(*) > 25").Find(&repoTopics); err != nil { return err } @@ -109,8 +157,8 @@ func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { for _, repoTopic := range repoTopics { touchedRepo[repoTopic.RepoID] = struct{}{} - tmpRepoTopics = tmpRepoTopics[:0] - if err := sess.Where("repo_id = ?", repoTopic.RepoID).Find(&tmpRepoTopics); err != nil { + tmpRepoTopics := make([]*RepoTopic, 0, 30) + if err := x.Where("repo_id = ?", repoTopic.RepoID).Find(&tmpRepoTopics); err != nil { return err } @@ -122,13 +170,18 @@ func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { } } + sess.Init() + log.Info("Deleting superfluous topics for repositories (more than 25 topics)...") + if err := sess.Begin(); err != nil { + return err + } for _, repoTopic := range delRepoTopics { log.Info("Deleting 'repo_topic' rows for 'repository' with id = %v. Topic id = %v", repoTopic.RepoID, repoTopic.TopicID) if _, err := sess.Where("repo_id = ? AND topic_id = ?", repoTopic.RepoID, - repoTopic.TopicID).Delete(&models.RepoTopic{}); err != nil { + repoTopic.TopicID).Delete(&RepoTopic{}); err != nil { return err } if _, err := sess.Exec( @@ -138,17 +191,17 @@ func reformatAndRemoveIncorrectTopics(x *xorm.Engine) (err error) { } } - topicNames := make([]string, 0, 30) log.Info("Updating repositories 'topics' fields...") for repoID := range touchedRepo { + topicNames := make([]string, 0, 30) if err := sess.Table("topic").Cols("name"). - Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id"). - Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil { + Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id"). + Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil { return err } log.Info("Updating 'topics' field for repository with id = %v", repoID) if _, err := sess.ID(repoID).Cols("topics"). - Update(&models.Repository{Topics: topicNames}); err != nil { + Update(&Repository{Topics: topicNames}); err != nil { return err } } diff --git a/models/topic.go b/models/topic.go index 247aac5ff..678795a3d 100644 --- a/models/topic.go +++ b/models/topic.go @@ -26,7 +26,7 @@ var topicPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // Topic represents a topic of repositories type Topic struct { ID int64 - Name string `xorm:"unique"` + Name string `xorm:"UNIQUE"` RepoCount int CreatedUnix util.TimeStamp `xorm:"INDEX created"` UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` @@ -34,8 +34,8 @@ type Topic struct { // RepoTopic represents associated repositories and topics type RepoTopic struct { - RepoID int64 `xorm:"unique(s)"` - TopicID int64 `xorm:"unique(s)"` + RepoID int64 `xorm:"UNIQUE(s)"` + TopicID int64 `xorm:"UNIQUE(s)"` } // ErrTopicNotExist represents an error that a topic is not exist @@ -190,10 +190,10 @@ func SaveTopics(repoID int64, topicNames ...string) error { } } - topicNames = topicNames[:0] + topicNames = make([]string, 0, 25) if err := sess.Table("topic").Cols("name"). - Join("INNER", "repo_topic", "topic.id = repo_topic.topic_id"). - Where("repo_topic.repo_id = ?", repoID).Find(&topicNames); err != nil { + Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id"). + Where("repo_topic.repo_id = ?", repoID).Desc("topic.repo_count").Find(&topicNames); err != nil { return err } diff --git a/modules/util/util.go b/modules/util/util.go index b6acb9796..5dcbe448f 100644 --- a/modules/util/util.go +++ b/modules/util/util.go @@ -10,6 +10,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" ) // OptionalBool a boolean that can be "null" @@ -78,6 +79,18 @@ func URLJoin(base string, elems ...string) string { return joinedURL } +// IsExternalURL checks if rawURL points to an external URL like http://example.com +func IsExternalURL(rawURL string) bool { + parsed, err := url.Parse(rawURL) + if err != nil { + return true + } + if len(parsed.Host) != 0 && strings.Replace(parsed.Host, "www.", "", 1) != strings.Replace(setting.Domain, "www.", "", 1) { + return true + } + return false +} + // Min min of two ints func Min(a, b int) int { if a > b { diff --git a/modules/util/util_test.go b/modules/util/util_test.go index 0d79df605..d9357ffa3 100644 --- a/modules/util/util_test.go +++ b/modules/util/util_test.go @@ -7,6 +7,8 @@ package util import ( "testing" + "code.gitea.io/gitea/modules/setting" + "github.com/stretchr/testify/assert" ) @@ -42,3 +44,36 @@ func TestURLJoin(t *testing.T) { assert.Equal(t, test.Expected, URLJoin(test.Base, test.Elements...)) } } + +func TestIsExternalURL(t *testing.T) { + setting.Domain = "try.gitea.io" + type test struct { + Expected bool + RawURL string + } + newTest := func(expected bool, rawURL string) test { + return test{Expected: expected, RawURL: rawURL} + } + for _, test := range []test{ + newTest(false, + "https://try.gitea.io"), + newTest(true, + "https://example.com/"), + newTest(true, + "//example.com"), + newTest(true, + "http://example.com"), + newTest(false, + "a/"), + newTest(false, + "https://try.gitea.io/test?param=false"), + newTest(false, + "test?param=false"), + newTest(false, + "//try.gitea.io/test?param=false"), + newTest(false, + "/hey/hey/hey#3244"), + } { + assert.Equal(t, test.Expected, IsExternalURL(test.RawURL)) + } +} diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index c2274b99a..1b0129ba1 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -54,13 +54,14 @@ password=Parole db_name=Datu bāzes nosaukums path=Ceļš -repo_path=Repozitoriju glabāšanas vieta -log_root_path=Žurnalizēšanas direktorija +repo_path=Repozitoriju glabāšanas ceļš +log_root_path=Žurnalizēšanas ceļš optional_title=Neobligātie iestatījumi smtp_host=SMTP resursdators federated_avatar_lookup_popup=Iespējot apvienoto profila bilžu meklētāju, lai izmantotu atvērtā koda apvienoto servisu balstītu uz libravatar. openid_signin=Iespējot OpenID autorizāciju +openid_signin_popup=Iespējot lietotāju autorizāciju ar OpenID. enable_captcha_popup=Lietotājam reģistrējoties, pieprasīt ievadīt drošības kodu. admin_password=Parole confirm_password=Apstipriniet paroli diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 0df553778..f2ec96dd0 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -1167,6 +1167,8 @@ branch.protected_deletion_failed=A branch '%s' está protegida. Ela não pode se topic.manage_topics=Gerenciar Tópicos topic.done=Feito +topic.count_prompt=Você não pode selecionar mais de 25 tópicos +topic.format_prompt=Tópicos devem começar com uma letra ou um número, podem incluir hífens (-) e não devem ter mais de 35 caracteres [org] org_name_holder=Nome da organização diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 008895181..9df7e4e16 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -1167,6 +1167,8 @@ branch.protected_deletion_failed=Ветка '%s' защищена. Её нель topic.manage_topics=Редактировать тематические метки topic.done=Сохранить +topic.count_prompt=Вы не можете выбрать более 25 тем +topic.format_prompt=Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов [org] org_name_holder=Название организации diff --git a/options/locale/locale_uk-UA.ini b/options/locale/locale_uk-UA.ini index b0d098bad..14d6335d8 100644 --- a/options/locale/locale_uk-UA.ini +++ b/options/locale/locale_uk-UA.ini @@ -1,4 +1,4 @@ -app_desc=Зручний сервіс, власного Git хостінгу +app_desc=Зручний сервіс, власного Git хостингу home=Головна dashboard=Панель управління @@ -40,6 +40,7 @@ u2f_unsupported_browser=Ваш браузер не підтримує U2F клю u2f_error_1=Сталася невідома помилка. Спробуйте ще раз. u2f_error_2=Переконайтеся, що ви використовуєте зашифроване з'єднання (https://) та відвідуєте правильну URL-адресу. u2f_error_3=Сервер не може обробити, ваш запит. +u2f_error_4=Представлений ключ не дає право на цей запит. Якщо спробуєте зареєструвати його, переконайтеся, що ключ ще не зареєстровано. u2f_error_5=Таймаут досягнуто до того, як ваш ключ можна буде прочитати. Перезавантажте, щоб повторити спробу. u2f_reload=Оновити @@ -74,7 +75,7 @@ cancel=Відмінити [install] install=Встановлення title=Початкова конфігурація -docker_helper=Якщо ви запускаєте Gitea всередині Docker, будь ласка уважно прочитайте документацію перед тим, як що-небудь змінити на цій сторінці. +docker_helper=Якщо ви запускаєте Gitea всередині Docker, будь ласка уважно прочитайте документацію перед тим, як щось змінити на цій сторінці. requite_db_desc=Gitea потребує MySQL, PostgreSQL, MSSQL, SQLite3 або TiDB. db_title=Налаштування бази даних db_type=Тип бази даних @@ -145,6 +146,7 @@ confirm_password=Підтвердження пароля admin_email=Адреса електронної пошти install_btn_confirm=Встановлення Gitea test_git_failed=Не в змозі перевірити 'git' команду: %v +sqlite3_not_available=Ця версія Gitea не підтримує SQLite3. Будь ласка, завантажте офіційну бінарну версію з %s (не версію gobuild). invalid_db_setting=Налаштування бази даних є некоректними: %v invalid_repo_path=Помилковий шлях до кореня репозиторію: %v save_config_failed=Не в змозі зберегти конфігурацію: %v @@ -294,6 +296,8 @@ unable_verify_ssh_key=Не вдається підтвердити ключ SSH; auth_failed=Помилка автентифікації: %v still_own_repo=Ваш обліковий запис володіє одним або декількома репозиторіями; видаліть або перенесіть їх в першу чергу. +still_has_org=Ваш обліковий запис є учасником однієї чи декількох організацій; вийдіть з них в першу чергу. +org_still_own_repo=Ця організація як і раніше володіє одним або декількома репозиторіями; спочатку видаліть або перенесіть їх. target_branch_not_exist=Цільової гілки не існує. @@ -367,6 +371,7 @@ primary=Основний primary_email=Зробити основним delete_email=Видалити email_deletion=Видалити адресу електронної пошти +email_deletion_success=Адресу електронної пошти було видалено. openid_deletion=Видалити адресу OpenID openid_deletion_success=Адреса OpenID була видалена. add_new_email=Додати нову адресу електронної пошти @@ -400,6 +405,7 @@ add_gpg_key_success=GPG ключ '%s' додано. delete_key=Видалити ssh_key_deletion=Видалити SSH ключ gpg_key_deletion=Видалити GPG ключ +ssh_key_deletion_desc=Видалення ключа SSH скасовує доступ до вашого облікового запису. Продовжити? gpg_key_deletion_desc=Видалення GPG ключа скасовує перевірку підписаних ним комітів. Продовжити? ssh_key_deletion_success=SSH було видалено. gpg_key_deletion_success=GPG було видалено. @@ -593,6 +599,7 @@ commits.signed_by=Підписано commits.gpg_key_id=Ідентифікатор GPG ключа ext_issues=Зов. Проблеми +ext_issues.desc=Посилання на зовнішню систему відстеження проблем. issues.new=Нова проблема issues.new.labels=Мітки @@ -614,6 +621,7 @@ issues.new_label_desc_placeholder=Опис issues.create_label=Створити мітку issues.label_templates.title=Завантажити визначений набір міток issues.label_templates.helper=Оберіть набір міток +issues.label_templates.use=Використовувати набір міток issues.label_templates.fail_to_load_file=Не вдалося завантажити файл шаблона мітки '%s': %v issues.add_label_at=додав(ла) мітку
%s
%s issues.add_milestone_at=`додав(ла) до %s етапу %s` @@ -621,6 +629,7 @@ issues.deleted_milestone=`(видалено)` issues.add_assignee_at=`був призначений %s %s` issues.remove_assignee_at=`видалили із призначених %s` issues.change_title_at=`змінив(ла) заголовок з %s на %s %s` +issues.delete_branch_at=`видалена гілка %s %s` issues.open_tab=%d відкрито issues.close_tab=%d закрито issues.filter_label=Мітка @@ -702,9 +711,11 @@ issues.start_tracking=Почати відстеження часу issues.start_tracking_history=`почав працювати %s` issues.tracking_already_started=`Ви вже почали відстежувати час для цієї проблеми!` issues.stop_tracking=Стоп +issues.stop_tracking_history=`перестав(-ла) працювати %s` issues.add_time=Вручну додати час issues.add_time_short=Додати час issues.add_time_cancel=Відмінити +issues.add_time_history=`додав(-ла) витрачений час %s` issues.add_time_hours=Години issues.add_time_minutes=Хвилини issues.add_time_sum_to_small=Час не введено. @@ -713,6 +724,8 @@ issues.cancel_tracking_history=`скасував відстеження часу issues.time_spent_total=Загальний витрачений час issues.time_spent_from_all_authors=`Загальний витрачений час: %s` issues.due_date=Дата завершення +issues.invalid_due_date_format=Дата закінчення має бути в форматі 'ррр-мм-дд'. +issues.error_modifying_due_date=Не вдалося змінити дату завершення. issues.due_date_form=рррр-мм-дд issues.due_date_form_add=Додати дату завершення issues.due_date_form_update=Оновити дату завершення @@ -768,6 +781,7 @@ milestones.filter_sort.most_issues=Найбільш проблем milestones.filter_sort.least_issues=Найменш проблем ext_wiki=Зов. Вікі +ext_wiki.desc=Посилання на зовнішню вікі. wiki=Вікі wiki.welcome=Ласкаво просимо до Вікі. @@ -817,6 +831,7 @@ activity.closed_issue_label=Закрито activity.new_issues_count_1=Нова Проблема activity.new_issues_count_n=%d Проблем activity.new_issue_label=Відкриті +activity.title.unresolved_conv_1=%d Незавершене обговорення activity.unresolved_conv_label=Відкрити activity.title.releases_1=%d Реліз activity.title.releases_n=%d Релізів diff --git a/routers/user/auth.go b/routers/user/auth.go index 9a59f52db..317b4af3b 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -18,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "github.com/go-macaron/captcha" "github.com/markbates/goth" @@ -474,7 +475,7 @@ func handleSignInFull(ctx *context.Context, u *models.User, remember bool, obeyR return setting.AppSubURL + "/" } - if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 { + if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 && !util.IsExternalURL(redirectTo) { ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) if obeyRedirect { ctx.RedirectToFirst(redirectTo)