diff --git a/Makefile b/Makefile index ffffb1faf0..f023608b1e 100644 --- a/Makefile +++ b/Makefile @@ -156,8 +156,8 @@ endif FORGEJO_API_SPEC := public/assets/forgejo/api.v1.yml SWAGGER_SPEC := templates/swagger/v1_json.tmpl -SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|g -SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape \| Safe}}/api/v1"|"basePath": "/api/v1"|g +SWAGGER_SPEC_S_TMPL := s|"basePath": *"/api/v1"|"basePath": "{{AppSubUrl \| JSEscape}}/api/v1"|g +SWAGGER_SPEC_S_JSON := s|"basePath": *"{{AppSubUrl \| JSEscape}}/api/v1"|"basePath": "/api/v1"|g SWAGGER_EXCLUDE := code.gitea.io/sdk SWAGGER_NEWLINE_COMMAND := -e '$$a\' SWAGGER_SPEC_BRANDING := s|Gitea API|Forgejo API|g diff --git a/modules/actions/github.go b/modules/actions/github.go index a988b2a124..d4e559408b 100644 --- a/modules/actions/github.go +++ b/modules/actions/github.go @@ -55,7 +55,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent case webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return true default: diff --git a/modules/actions/workflows.go b/modules/actions/workflows.go index 00d83e06d7..81ab26bc27 100644 --- a/modules/actions/workflows.go +++ b/modules/actions/workflows.go @@ -186,7 +186,9 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web webhook_module.HookEventPullRequest, webhook_module.HookEventPullRequestSync, webhook_module.HookEventPullRequestAssign, - webhook_module.HookEventPullRequestLabel: + webhook_module.HookEventPullRequestLabel, + webhook_module.HookEventPullRequestReviewRequest, + webhook_module.HookEventPullRequestMilestone: return matchPullRequestEvent(gitRepo, commit, payload.(*api.PullRequestPayload), evt) case // pull_request_review @@ -362,13 +364,13 @@ func matchPullRequestEvent(gitRepo *git.Repository, commit *git.Commit, prPayloa } else { // See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request // Actions with the same name: - // opened, edited, closed, reopened, assigned, unassigned + // opened, edited, closed, reopened, assigned, unassigned, review_requested, review_request_removed, milestoned, demilestoned // Actions need to be converted: // synchronized -> synchronize // label_updated -> labeled // label_cleared -> unlabeled // Unsupported activity types: - // converted_to_draft, ready_for_review, locked, unlocked, review_requested, review_request_removed, auto_merge_enabled, auto_merge_disabled + // converted_to_draft, ready_for_review, locked, unlocked, auto_merge_enabled, auto_merge_disabled, enqueued, dequeued action := prPayload.Action switch action { diff --git a/modules/context/context_response.go b/modules/context/context_response.go index d9102b77bd..829bca1f59 100644 --- a/modules/context/context_response.go +++ b/modules/context/context_response.go @@ -90,6 +90,20 @@ func (ctx *Context) HTML(status int, name base.TplName) { } } +// JSONTemplate renders the template as JSON response +// keep in mind that the template is processed in HTML context, so JSON-things should be handled carefully, eg: by JSEscape +func (ctx *Context) JSONTemplate(tmpl base.TplName) { + t, err := ctx.Render.TemplateLookup(string(tmpl), nil) + if err != nil { + ctx.ServerError("unable to find template", err) + return + } + ctx.Resp.Header().Set("Content-Type", "application/json") + if err = t.Execute(ctx.Resp, ctx.Data); err != nil { + ctx.ServerError("unable to execute template", err) + } +} + // RenderToString renders the template content to a string func (ctx *Context) RenderToString(name base.TplName, data map[string]any) (string, error) { var buf strings.Builder diff --git a/modules/context/context_template.go b/modules/context/context_template.go index ba90fc170a..7878d409ca 100644 --- a/modules/context/context_template.go +++ b/modules/context/context_template.go @@ -5,10 +5,7 @@ package context import ( "context" - "errors" "time" - - "code.gitea.io/gitea/modules/log" ) var _ context.Context = TemplateContext(nil) @@ -36,14 +33,3 @@ func (c TemplateContext) Err() error { func (c TemplateContext) Value(key any) any { return c.parentContext().Value(key) } - -// DataRaceCheck checks whether the template context function "ctx()" returns the consistent context -// as the current template's rendering context (request context), to help to find data race issues as early as possible. -// When the code is proven to be correct and stable, this function should be removed. -func (c TemplateContext) DataRaceCheck(dataCtx context.Context) (string, error) { - if c.parentContext() != dataCtx { - log.Error("TemplateContext.DataRaceCheck: parent context mismatch\n%s", log.Stack(2)) - return "", errors.New("parent context mismatch") - } - return "", nil -} diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 90371fa8fe..3bf1919b4a 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -38,7 +38,7 @@ func NewFuncMap() template.FuncMap { "Safe": Safe, "Escape": Escape, "QueryEscape": url.QueryEscape, - "JSEscape": template.JSEscapeString, + "JSEscape": JSEscapeSafe, "Str2html": Str2html, // TODO: rename it to SanitizeHTML "URLJoin": util.URLJoin, "DotEscape": DotEscape, @@ -214,6 +214,10 @@ func Escape(s any) template.HTML { panic(fmt.Sprintf("unexpected type %T", s)) } +func JSEscapeSafe(s string) template.HTML { + return template.HTML(template.JSEscapeString(s)) +} + func RenderEmojiPlain(s any) any { switch v := s.(type) { case string: diff --git a/modules/templates/helper_test.go b/modules/templates/helper_test.go index ec83e9ac33..739a92f34f 100644 --- a/modules/templates/helper_test.go +++ b/modules/templates/helper_test.go @@ -52,3 +52,7 @@ func TestSubjectBodySeparator(t *testing.T) { "", "Insuficient\n--\nSeparators") } + +func TestJSEscapeSafe(t *testing.T) { + assert.EqualValues(t, `\u0026\u003C\u003E\'\"`, JSEscapeSafe(`&<>'"`)) +} diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index ffe69a74df..b364992dfe 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -4,6 +4,7 @@ package i18n import ( + "html/template" "strings" "testing" @@ -82,6 +83,71 @@ c=22 assert.Equal(t, "22", lang1.TrString("c")) } +type stringerPointerReceiver struct { + s string +} + +func (s *stringerPointerReceiver) String() string { + return s.s +} + +type stringerStructReceiver struct { + s string +} + +func (s stringerStructReceiver) String() string { + return s.s +} + +type errorStructReceiver struct { + s string +} + +func (e errorStructReceiver) Error() string { + return e.s +} + +type errorPointerReceiver struct { + s string +} + +func (e *errorPointerReceiver) Error() string { + return e.s +} + +func TestLocaleWithTemplate(t *testing.T) { + ls := NewLocaleStore() + assert.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=%s`), nil)) + lang1, _ := ls.Locale("lang1") + + tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) + tmpl = template.Must(tmpl.Parse(`{{tr "key" .var}}`)) + + cases := []struct { + in any + want string + }{ + {"", "<str>"}, + {[]byte(""), "[60 98 121 116 101 115 62]"}, + {template.HTML(""), ""}, + {stringerPointerReceiver{""}, "{<stringerPointerReceiver>}"}, + {&stringerPointerReceiver{""}, "<stringerPointerReceiver ptr>"}, + {stringerStructReceiver{""}, "<stringerStructReceiver>"}, + {&stringerStructReceiver{""}, "<stringerStructReceiver ptr>"}, + {errorStructReceiver{""}, "<errorStructReceiver>"}, + {&errorStructReceiver{""}, "<errorStructReceiver ptr>"}, + {errorPointerReceiver{""}, "{<errorPointerReceiver>}"}, + {&errorPointerReceiver{""}, "<errorPointerReceiver ptr>"}, + } + + buf := &strings.Builder{} + for _, c := range cases { + buf.Reset() + assert.NoError(t, tmpl.Execute(buf, map[string]any{"var": c.in})) + assert.Equal(t, c.want, buf.String()) + } +} + func TestLocaleStoreQuirks(t *testing.T) { const nl = "\n" q := func(q1, s string, q2 ...string) string { diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index d0b5e05649..44c3fb0c88 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -136,12 +136,14 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { args := slices.Clone(trArgs) for i, v := range args { switch v := v.(type) { + case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: + // for most basic types (including template.HTML which is safe), just do nothing and use it case string: - args[i] = template.HTML(template.HTMLEscapeString(v)) + args[i] = template.HTMLEscapeString(v) case fmt.Stringer: args[i] = template.HTMLEscapeString(v.String()) - default: // int, float, include template.HTML - // do nothing, just use it + default: + args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } return template.HTML(l.TrString(trKey, args...)) diff --git a/options/license/Brian-Gladman-2-Clause b/options/license/Brian-Gladman-2-Clause new file mode 100644 index 0000000000..7276f63e9e --- /dev/null +++ b/options/license/Brian-Gladman-2-Clause @@ -0,0 +1,17 @@ +Copyright (C) 1998-2013, Brian Gladman, Worcester, UK. All + rights reserved. + +The redistribution and use of this software (with or without +changes) is allowed without the payment of fees or royalties +provided that: + + source code distributions include the above copyright notice, + this list of conditions and the following disclaimer; + + binary distributions include the above copyright notice, this + list of conditions and the following disclaimer in their + documentation. + +This software is provided 'as is' with no explicit or implied +warranties in respect of its operation, including, but not limited +to, correctness and fitness for purpose. diff --git a/options/license/CMU-Mach-nodoc b/options/license/CMU-Mach-nodoc new file mode 100644 index 0000000000..c81d74fee7 --- /dev/null +++ b/options/license/CMU-Mach-nodoc @@ -0,0 +1,11 @@ +Copyright (C) 2002 Naval Research Laboratory (NRL/CCS) + +Permission to use, copy, modify and distribute this software and +its documentation is hereby granted, provided that both the +copyright notice and this permission notice appear in all copies of +the software, derivative works or modified versions, and any +portions thereof. + +NRL ALLOWS FREE USE OF THIS SOFTWARE IN ITS "AS IS" CONDITION AND +DISCLAIMS ANY LIABILITY OF ANY KIND FOR ANY DAMAGES WHATSOEVER +RESULTING FROM THE USE OF THIS SOFTWARE. diff --git a/options/license/GNOME-examples-exception b/options/license/GNOME-examples-exception new file mode 100644 index 0000000000..0f0cd53b50 --- /dev/null +++ b/options/license/GNOME-examples-exception @@ -0,0 +1 @@ +As a special exception, the copyright holders give you permission to copy, modify, and distribute the example code contained in this document under the terms of your choosing, without restriction. diff --git a/options/license/Gmsh-exception b/options/license/Gmsh-exception new file mode 100644 index 0000000000..6d28f704e4 --- /dev/null +++ b/options/license/Gmsh-exception @@ -0,0 +1,16 @@ +The copyright holders of Gmsh give you permission to combine Gmsh + with code included in the standard release of Netgen (from Joachim + Sch"oberl), METIS (from George Karypis at the University of + Minnesota), OpenCASCADE (from Open CASCADE S.A.S) and ParaView + (from Kitware, Inc.) under their respective licenses. You may copy + and distribute such a system following the terms of the GNU GPL for + Gmsh and the licenses of the other code concerned, provided that + you include the source code of that other code when and as the GNU + GPL requires distribution of source code. + + Note that people who make modified versions of Gmsh are not + obligated to grant this special exception for their modified + versions; it is their choice whether to do so. The GNU General + Public License gives permission to release a modified version + without this exception; this exception also makes it possible to + release a modified version which carries forward this exception. diff --git a/options/license/HPND-Fenneberg-Livingston b/options/license/HPND-Fenneberg-Livingston new file mode 100644 index 0000000000..aaf524f3aa --- /dev/null +++ b/options/license/HPND-Fenneberg-Livingston @@ -0,0 +1,13 @@ +Copyright (C) 1995,1996,1997,1998 Lars Fenneberg + +Permission to use, copy, modify, and distribute this software for any +purpose and without fee is hereby granted, provided that this copyright and +permission notice appear on all copies and supporting documentation, the +name of Lars Fenneberg not be used in advertising or publicity pertaining to +distribution of the program without specific prior permission, and notice be +given in supporting documentation that copying and distribution is by +permission of Lars Fenneberg. + +Lars Fenneberg makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or implied +warranty. diff --git a/options/license/HPND-INRIA-IMAG b/options/license/HPND-INRIA-IMAG new file mode 100644 index 0000000000..87d09d92cb --- /dev/null +++ b/options/license/HPND-INRIA-IMAG @@ -0,0 +1,9 @@ +This software is available with usual "research" terms with +the aim of retain credits of the software. Permission to use, +copy, modify and distribute this software for any purpose and +without fee is hereby granted, provided that the above copyright +notice and this permission notice appear in all copies, and +the name of INRIA, IMAG, or any contributor not be used in +advertising or publicity pertaining to this material without +the prior explicit permission. The software is provided "as +is" without any warranties, support or liabilities of any kind. diff --git a/options/license/Mackerras-3-Clause b/options/license/Mackerras-3-Clause new file mode 100644 index 0000000000..6467f0c98e --- /dev/null +++ b/options/license/Mackerras-3-Clause @@ -0,0 +1,25 @@ +Copyright (c) 1995 Eric Rosenquist. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + 3. The name(s) of the authors of this software must not be used to + endorse or promote products derived from this software without + prior written permission. + + THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO + THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/options/license/Mackerras-3-Clause-acknowledgment b/options/license/Mackerras-3-Clause-acknowledgment new file mode 100644 index 0000000000..5f0187add7 --- /dev/null +++ b/options/license/Mackerras-3-Clause-acknowledgment @@ -0,0 +1,25 @@ +Copyright (c) 1993-2002 Paul Mackerras. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The name(s) of the authors of this software must not be used to + endorse or promote products derived from this software without + prior written permission. + +3. Redistributions of any form whatsoever must retain the following + acknowledgment: + "This product includes software developed by Paul Mackerras + ". + +THE AUTHORS OF THIS SOFTWARE DISCLAIM ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/options/license/OpenVision b/options/license/OpenVision new file mode 100644 index 0000000000..983505389e --- /dev/null +++ b/options/license/OpenVision @@ -0,0 +1,33 @@ +Copyright, OpenVision Technologies, Inc., 1993-1996, All Rights +Reserved + +WARNING: Retrieving the OpenVision Kerberos Administration system +source code, as described below, indicates your acceptance of the +following terms. If you do not agree to the following terms, do +not retrieve the OpenVision Kerberos administration system. + +You may freely use and distribute the Source Code and Object Code +compiled from it, with or without modification, but this Source +Code is provided to you "AS IS" EXCLUSIVE OF ANY WARRANTY, +INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY OR +FITNESS FOR A PARTICULAR PURPOSE, OR ANY OTHER WARRANTY, WHETHER +EXPRESS OR IMPLIED. IN NO EVENT WILL OPENVISION HAVE ANY LIABILITY +FOR ANY LOST PROFITS, LOSS OF DATA OR COSTS OF PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES, OR FOR ANY SPECIAL, INDIRECT, OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THIS AGREEMENT, INCLUDING, +WITHOUT LIMITATION, THOSE RESULTING FROM THE USE OF THE SOURCE +CODE, OR THE FAILURE OF THE SOURCE CODE TO PERFORM, OR FOR ANY +OTHER REASON. + +OpenVision retains all copyrights in the donated Source Code. +OpenVision also retains copyright to derivative works of the Source +Code, whether created by OpenVision or by a third party. The +OpenVision copyright notice must be preserved if derivative works +are made based on the donated Source Code. + +OpenVision Technologies, Inc. has donated this Kerberos +Administration system to MIT for inclusion in the standard Kerberos +5 distribution. This donation underscores our commitment to +continuing Kerberos technology development and our gratitude for +the valuable work which has been performed by MIT and the Kerberos +community. diff --git a/options/license/Sun-PPP b/options/license/Sun-PPP new file mode 100644 index 0000000000..5f94a13437 --- /dev/null +++ b/options/license/Sun-PPP @@ -0,0 +1,13 @@ +Copyright (c) 2001 by Sun Microsystems, Inc. +All rights reserved. + +Non-exclusive rights to redistribute, modify, translate, and use +this software in source and binary forms, in whole or in part, is +hereby granted, provided that the above copyright notice is +duplicated in any source form, and that neither the name of the +copyright holder nor the author is used to endorse or promote +products derived from this software. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED +WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. diff --git a/options/license/UMich-Merit b/options/license/UMich-Merit new file mode 100644 index 0000000000..93e304b90e --- /dev/null +++ b/options/license/UMich-Merit @@ -0,0 +1,19 @@ +[C] The Regents of the University of Michigan and Merit Network, Inc. 1992, +1993, 1994, 1995 All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided +that the above copyright notice and this permission notice appear in all +copies of the software and derivative works or modified versions thereof, +and that both the copyright notice and this permission and disclaimer +notice appear in supporting documentation. + +THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE REGENTS OF THE +UNIVERSITY OF MICHIGAN AND MERIT NETWORK, INC. DO NOT WARRANT THAT THE +FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET LICENSEE'S REQUIREMENTS OR +THAT OPERATION WILL BE UNINTERRUPTED OR ERROR FREE. The Regents of the +University of Michigan and Merit Network, Inc. shall not be liable for any +special, indirect, incidental or consequential damages with respect to any +claim by Licensee or any third party arising from use of the software. diff --git a/options/license/bcrypt-Solar-Designer b/options/license/bcrypt-Solar-Designer new file mode 100644 index 0000000000..8cb05017fc --- /dev/null +++ b/options/license/bcrypt-Solar-Designer @@ -0,0 +1,11 @@ +Written by Solar Designer in 1998-2014. +No copyright is claimed, and the software is hereby placed in the public +domain. In case this attempt to disclaim copyright and place the software +in the public domain is deemed null and void, then the software is +Copyright (c) 1998-2014 Solar Designer and it is hereby released to the +general public under the following terms: + +Redistribution and use in source and binary forms, with or without +modification, are permitted. + +There's ABSOLUTELY NO WARRANTY, express or implied. diff --git a/options/license/gtkbook b/options/license/gtkbook new file mode 100644 index 0000000000..91215e80d6 --- /dev/null +++ b/options/license/gtkbook @@ -0,0 +1,6 @@ +Copyright 2005 Syd Logan, All Rights Reserved + +This code is distributed without warranty. You are free to use +this code for any purpose, however, if this code is republished or +redistributed in its original form, as hardcopy or electronically, +then you must include this copyright notice along with the code. diff --git a/options/license/softSurfer b/options/license/softSurfer new file mode 100644 index 0000000000..1bbc88c34c --- /dev/null +++ b/options/license/softSurfer @@ -0,0 +1,6 @@ +Copyright 2001, softSurfer (www.softsurfer.com) +This code may be freely used and modified for any purpose +providing that this copyright notice is included with it. +SoftSurfer makes no warranty for this code, and cannot be held +liable for any real or imagined damage resulting from its use. +Users of this code must verify correctness for their application. diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 98ec0c18c9..bdae9a29ac 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2023,15 +2023,10 @@ activity.git_stats_and_deletions = and activity.git_stats_deletion_1 = %d deletion activity.git_stats_deletion_n = %d deletions -contributors = Contributors contributors.contribution_type.filter_label = Contribution type: contributors.contribution_type.commits = Commits contributors.contribution_type.additions = Additions contributors.contribution_type.deletions = Deletions -contributors.loading_title = Loading contributions... -contributors.loading_title_failed = Could not load contributions -contributors.loading_info = This might take a bit… -contributors.component_failed_to_load = An unexpected error happened. search = Search search.search_repo = Search repository @@ -2652,6 +2647,13 @@ error.csv.too_large = Can't render this file because it is too large. error.csv.unexpected = Can't render this file because it contains an unexpected character in line %d and column %d. error.csv.invalid_field_count = Can't render this file because it has a wrong number of fields in line %d. +[graphs] +component_loading = Loading %s... +component_loading_failed = Could not load %s +component_loading_info = This might take a bit… +component_failed_to_load = An unexpected error happened. +contributors.what = contributions + [org] org_name_holder = Organization Name org_full_name_holder = Organization Full Name diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 58814d3b2e..1babccb650 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -8,7 +8,7 @@ // // Schemes: https, http // BasePath: /api/v1 -// Version: {{AppVer | JSEscape | Safe}} +// Version: {{AppVer | JSEscape}} // License: MIT http://opensource.org/licenses/MIT // // Consumes: diff --git a/routers/web/auth/oauth.go b/routers/web/auth/oauth.go index 2adcb3aea0..e840e03bcf 100644 --- a/routers/web/auth/oauth.go +++ b/routers/web/auth/oauth.go @@ -579,16 +579,8 @@ func GrantApplicationOAuth(ctx *context.Context) { // OIDCWellKnown generates JSON so OIDC clients know Gitea's capabilities func OIDCWellKnown(ctx *context.Context) { - t, err := ctx.Render.TemplateLookup("user/auth/oidc_wellknown", nil) - if err != nil { - ctx.ServerError("unable to find template", err) - return - } - ctx.Resp.Header().Set("Content-Type", "application/json") ctx.Data["SigningKey"] = oauth2.DefaultSigningKey - if err = t.Execute(ctx.Resp, ctx.Data); err != nil { - ctx.ServerError("unable to execute template", err) - } + ctx.JSONTemplate("user/auth/oidc_wellknown") } // OIDCKeys generates the JSON Web Key Set diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go index f7dedc0b34..bcfef7580a 100644 --- a/routers/web/repo/contributors.go +++ b/routers/web/repo/contributors.go @@ -18,7 +18,7 @@ const ( // Contributors render the page to show repository contributors graph func Contributors(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("repo.contributors") + ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.contributors") ctx.Data["PageIsActivity"] = true ctx.Data["PageIsContributors"] = true diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index ca854a35f2..ab821f8884 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -662,6 +662,24 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C } if pb != nil && pb.EnableStatusCheck { + + var missingRequiredChecks []string + for _, requiredContext := range pb.StatusCheckContexts { + contextFound := false + matchesRequiredContext := createRequiredContextMatcher(requiredContext) + for _, presentStatus := range commitStatuses { + if matchesRequiredContext(presentStatus.Context) { + contextFound = true + break + } + } + + if !contextFound { + missingRequiredChecks = append(missingRequiredChecks, requiredContext) + } + } + ctx.Data["MissingRequiredChecks"] = missingRequiredChecks + ctx.Data["is_context_required"] = func(context string) bool { for _, c := range pb.StatusCheckContexts { if c == context { @@ -730,6 +748,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.C return compareInfo } +func createRequiredContextMatcher(requiredContext string) func(string) bool { + if gp, err := glob.Compile(requiredContext); err == nil { + return func(contextToCheck string) bool { + return gp.Match(contextToCheck) + } + } + + return func(contextToCheck string) bool { + return requiredContext == contextToCheck + } +} + type pullCommitList struct { Commits []pull_service.CommitInfo `json:"commits"` LastReviewCommitSha string `json:"last_review_commit_sha"` diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go index 4d139f2b79..33e7f739ff 100644 --- a/routers/web/repo/release.go +++ b/routers/web/repo/release.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" @@ -67,6 +68,88 @@ func calReleaseNumCommitsBehind(repoCtx *context.Repository, release *repo_model return nil } +type ReleaseInfo struct { + Release *repo_model.Release + CommitStatus *git_model.CommitStatus + CommitStatuses []*git_model.CommitStatus +} + +func getReleaseInfos(ctx *context.Context, opts *repo_model.FindReleasesOptions) ([]*ReleaseInfo, error) { + releases, err := db.Find[repo_model.Release](ctx, opts) + if err != nil { + return nil, err + } + + for _, release := range releases { + release.Repo = ctx.Repo.Repository + } + + if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil { + return nil, err + } + + // Temporary cache commits count of used branches to speed up. + countCache := make(map[string]int64) + cacheUsers := make(map[int64]*user_model.User) + if ctx.Doer != nil { + cacheUsers[ctx.Doer.ID] = ctx.Doer + } + var ok bool + + canReadActions := ctx.Repo.CanRead(unit.TypeActions) + + releaseInfos := make([]*ReleaseInfo, 0, len(releases)) + for _, r := range releases { + if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { + r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) + if err != nil { + if user_model.IsErrUserNotExist(err) { + r.Publisher = user_model.NewGhostUser() + } else { + return nil, err + } + } + cacheUsers[r.PublisherID] = r.Publisher + } + + r.Note, err = markdown.RenderString(&markup.RenderContext{ + Links: markup.Links{ + Base: ctx.Repo.RepoLink, + }, + Metas: ctx.Repo.Repository.ComposeMetas(ctx), + GitRepo: ctx.Repo.GitRepo, + Ctx: ctx, + }, r.Note) + if err != nil { + return nil, err + } + + if !r.IsDraft { + if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { + return nil, err + } + } + + info := &ReleaseInfo{ + Release: r, + } + + if canReadActions { + statuses, _, err := git_model.GetLatestCommitStatus(ctx, r.Repo.ID, r.Sha1, db.ListOptions{ListAll: true}) + if err != nil { + return nil, err + } + + info.CommitStatus = git_model.CalcCommitStatus(statuses) + info.CommitStatuses = statuses + } + + releaseInfos = append(releaseInfos, info) + } + + return releaseInfos, nil +} + // Releases render releases list page func Releases(ctx *context.Context) { ctx.Data["PageIsReleaseList"] = true @@ -91,77 +174,21 @@ func Releases(ctx *context.Context) { writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived - opts := repo_model.FindReleasesOptions{ + releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ ListOptions: listOptions, // only show draft releases for users who can write, read-only users shouldn't see draft releases. IncludeDrafts: writeAccess, RepoID: ctx.Repo.Repository.ID, - } - - releases, err := db.Find[repo_model.Release](ctx, opts) + }) if err != nil { - ctx.ServerError("GetReleasesByRepoID", err) + ctx.ServerError("getReleaseInfos", err) return } - for _, release := range releases { - release.Repo = ctx.Repo.Repository - } - - if err = repo_model.GetReleaseAttachments(ctx, releases...); err != nil { - ctx.ServerError("GetReleaseAttachments", err) - return - } - - // Temporary cache commits count of used branches to speed up. - countCache := make(map[string]int64) - cacheUsers := make(map[int64]*user_model.User) - if ctx.Doer != nil { - cacheUsers[ctx.Doer.ID] = ctx.Doer - } - var ok bool - - for _, r := range releases { - if r.Publisher, ok = cacheUsers[r.PublisherID]; !ok { - r.Publisher, err = user_model.GetUserByID(ctx, r.PublisherID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - r.Publisher = user_model.NewGhostUser() - } else { - ctx.ServerError("GetUserByID", err) - return - } - } - cacheUsers[r.PublisherID] = r.Publisher - } - - r.Note, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, r.Note) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - if r.IsDraft { - continue - } - - if err := calReleaseNumCommitsBehind(ctx.Repo, r, countCache); err != nil { - ctx.ServerError("calReleaseNumCommitsBehind", err) - return - } - } - ctx.Data["Releases"] = releases numReleases := ctx.Data["NumReleases"].(int64) - pager := context.NewPagination(int(numReleases), opts.PageSize, opts.Page, 5) + pager := context.NewPagination(int(numReleases), listOptions.PageSize, listOptions.Page, 5) pager.SetDefaultParams(ctx) ctx.Data["Page"] = pager @@ -249,15 +276,24 @@ func SingleRelease(ctx *context.Context) { writeAccess := ctx.Repo.CanWrite(unit.TypeReleases) ctx.Data["CanCreateRelease"] = writeAccess && !ctx.Repo.Repository.IsArchived - release, err := repo_model.GetRelease(ctx, ctx.Repo.Repository.ID, ctx.Params("*")) + releases, err := getReleaseInfos(ctx, &repo_model.FindReleasesOptions{ + ListOptions: db.ListOptions{Page: 1, PageSize: 1}, + RepoID: ctx.Repo.Repository.ID, + TagNames: []string{ctx.Params("*")}, + // only show draft releases for users who can write, read-only users shouldn't see draft releases. + IncludeDrafts: writeAccess, + }) if err != nil { - if repo_model.IsErrReleaseNotExist(err) { - ctx.NotFound("GetRelease", err) - return - } - ctx.ServerError("GetReleasesByRepoID", err) + ctx.ServerError("getReleaseInfos", err) return } + if len(releases) != 1 { + ctx.NotFound("SingleRelease", err) + return + } + + release := releases[0].Release + ctx.Data["PageIsSingleTag"] = release.IsTag if release.IsTag { ctx.Data["Title"] = release.TagName @@ -265,43 +301,7 @@ func SingleRelease(ctx *context.Context) { ctx.Data["Title"] = release.Title } - release.Repo = ctx.Repo.Repository - - err = repo_model.GetReleaseAttachments(ctx, release) - if err != nil { - ctx.ServerError("GetReleaseAttachments", err) - return - } - - release.Publisher, err = user_model.GetUserByID(ctx, release.PublisherID) - if err != nil { - if user_model.IsErrUserNotExist(err) { - release.Publisher = user_model.NewGhostUser() - } else { - ctx.ServerError("GetUserByID", err) - return - } - } - if !release.IsDraft { - if err := calReleaseNumCommitsBehind(ctx.Repo, release, make(map[string]int64)); err != nil { - ctx.ServerError("calReleaseNumCommitsBehind", err) - return - } - } - release.Note, err = markdown.RenderString(&markup.RenderContext{ - Links: markup.Links{ - Base: ctx.Repo.RepoLink, - }, - Metas: ctx.Repo.Repository.ComposeMetas(ctx), - GitRepo: ctx.Repo.GitRepo, - Ctx: ctx, - }, release.Note) - if err != nil { - ctx.ServerError("RenderString", err) - return - } - - ctx.Data["Releases"] = []*repo_model.Release{release} + ctx.Data["Releases"] = releases ctx.HTML(http.StatusOK, tplReleasesList) } diff --git a/routers/web/shared/user/header.go b/routers/web/shared/user/header.go index 203e5fa5a5..6830bdb8a9 100644 --- a/routers/web/shared/user/header.go +++ b/routers/web/shared/user/header.go @@ -4,6 +4,8 @@ package user import ( + "net/url" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" access_model "code.gitea.io/gitea/models/perm/access" @@ -37,7 +39,7 @@ func PrepareContextForProfileBigAvatar(ctx *context.Context) { ctx.Data["IsFollowing"] = ctx.Doer != nil && user_model.IsFollowing(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["IsBlocked"] = ctx.Doer != nil && user_model.IsBlocked(ctx, ctx.Doer.ID, ctx.ContextUser.ID) ctx.Data["ShowUserEmail"] = setting.UI.ShowUserEmail && ctx.ContextUser.Email != "" && ctx.IsSigned && !ctx.ContextUser.KeepEmailPrivate - ctx.Data["UserLocationMapURL"] = setting.Service.UserLocationMapURL + ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location) // Show OpenID URIs openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID) diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go index 493c97aa67..42e9dbe967 100644 --- a/routers/web/swagger_json.go +++ b/routers/web/swagger_json.go @@ -4,22 +4,10 @@ package web import ( - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" ) -// tplSwaggerV1Json swagger v1 json template -const tplSwaggerV1Json base.TplName = "swagger/v1_json" - // SwaggerV1Json render swagger v1 json func SwaggerV1Json(ctx *context.Context) { - t, err := ctx.Render.TemplateLookup(string(tplSwaggerV1Json), nil) - if err != nil { - ctx.ServerError("unable to find template", err) - return - } - ctx.Resp.Header().Set("Content-Type", "application/json") - if err = t.Execute(ctx.Resp, ctx.Data); err != nil { - ctx.ServerError("unable to execute template", err) - } + ctx.JSONTemplate("swagger/v1_json") } diff --git a/services/actions/commit_status.go b/services/actions/commit_status.go index 72a3ab7ac6..edd1fd1568 100644 --- a/services/actions/commit_status.go +++ b/services/actions/commit_status.go @@ -64,6 +64,9 @@ func createCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er return fmt.Errorf("head of pull request is missing in event payload") } sha = payload.PullRequest.Head.Sha + case webhook_module.HookEventRelease: + event = string(run.Event) + sha = run.CommitSHA default: return nil } diff --git a/services/actions/notifier.go b/services/actions/notifier.go index 0b4fed5db1..77848a3f58 100644 --- a/services/actions/notifier.go +++ b/services/actions/notifier.go @@ -55,6 +55,47 @@ func (n *actionsNotifier) NewIssue(ctx context.Context, issue *issues_model.Issu }).Notify(withMethod(ctx, "NewIssue")) } +// IssueChangeContent notifies change content of issue +func (n *actionsNotifier) IssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string) { + ctx = withMethod(ctx, "IssueChangeContent") + + var err error + if err = issue.LoadRepo(ctx); err != nil { + log.Error("LoadRepo: %v", err) + return + } + + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + if issue.IsPull { + if err = issue.LoadPullRequest(ctx); err != nil { + log.Error("loadPullRequest: %v", err) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequest). + WithDoer(doer). + WithPayload(&api.PullRequestPayload{ + Action: api.HookIssueEdited, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), + Sender: convert.ToUser(ctx, doer, nil), + }). + WithPullRequest(issue.PullRequest). + Notify(ctx) + return + } + newNotifyInputFromIssue(issue, webhook_module.HookEventIssues). + WithDoer(doer). + WithPayload(&api.IssuePayload{ + Action: api.HookIssueEdited, + Index: issue.Index, + Issue: convert.ToAPIIssue(ctx, issue), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }). + Notify(ctx) +} + // IssueChangeStatus notifies close or reopen issue to notifiers func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, _ *issues_model.Comment, isClosed bool) { ctx = withMethod(ctx, "IssueChangeStatus") @@ -101,11 +142,40 @@ func (n *actionsNotifier) IssueChangeStatus(ctx context.Context, doer *user_mode Notify(ctx) } +// IssueChangeAssignee notifies assigned or unassigned to notifiers +func (n *actionsNotifier) IssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) { + ctx = withMethod(ctx, "IssueChangeAssignee") + + var action api.HookIssueAction + if removed { + action = api.HookIssueUnassigned + } else { + action = api.HookIssueAssigned + } + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestAssign, action) +} + +// IssueChangeMilestone notifies assignee to notifiers +func (n *actionsNotifier) IssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64) { + ctx = withMethod(ctx, "IssueChangeMilestone") + + var action api.HookIssueAction + if issue.MilestoneID > 0 { + action = api.HookIssueMilestoned + } else { + action = api.HookIssueDemilestoned + } + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestMilestone, action) +} + func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, _, _ []*issues_model.Label, ) { ctx = withMethod(ctx, "IssueChangeLabels") + notifyIssueChange(ctx, doer, issue, webhook_module.HookEventPullRequestLabel, api.HookIssueLabelUpdated) +} +func notifyIssueChange(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, event webhook_module.HookEventType, action api.HookIssueAction) { var err error if err = issue.LoadRepo(ctx); err != nil { log.Error("LoadRepo: %v", err) @@ -117,20 +187,15 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode return } - permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) if issue.IsPull { if err = issue.LoadPullRequest(ctx); err != nil { log.Error("loadPullRequest: %v", err) return } - if err = issue.PullRequest.LoadIssue(ctx); err != nil { - log.Error("LoadIssue: %v", err) - return - } - newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestLabel). + newNotifyInputFromIssue(issue, event). WithDoer(doer). WithPayload(&api.PullRequestPayload{ - Action: api.HookIssueLabelUpdated, + Action: action, Index: issue.Index, PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), Repository: convert.ToRepo(ctx, issue.Repo, access_model.Permission{AccessMode: perm_model.AccessModeNone}), @@ -140,10 +205,11 @@ func (n *actionsNotifier) IssueChangeLabels(ctx context.Context, doer *user_mode Notify(ctx) return } - newNotifyInputFromIssue(issue, webhook_module.HookEventIssueLabel). + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, issue.Poster) + newNotifyInputFromIssue(issue, event). WithDoer(doer). WithPayload(&api.IssuePayload{ - Action: api.HookIssueLabelUpdated, + Action: action, Index: issue.Index, Issue: convert.ToAPIIssue(ctx, issue), Repository: convert.ToRepo(ctx, issue.Repo, permission), @@ -305,6 +371,39 @@ func (n *actionsNotifier) PullRequestReview(ctx context.Context, pr *issues_mode }).Notify(ctx) } +func (n *actionsNotifier) PullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) { + if !issue.IsPull { + log.Warn("PullRequestReviewRequest: issue is not a pull request: %v", issue.ID) + return + } + + ctx = withMethod(ctx, "PullRequestReviewRequest") + + permission, _ := access_model.GetUserRepoPermission(ctx, issue.Repo, doer) + if err := issue.LoadPullRequest(ctx); err != nil { + log.Error("LoadPullRequest failed: %v", err) + return + } + var action api.HookIssueAction + if isRequest { + action = api.HookIssueReviewRequested + } else { + action = api.HookIssueReviewRequestRemoved + } + newNotifyInputFromIssue(issue, webhook_module.HookEventPullRequestReviewRequest). + WithDoer(doer). + WithPayload(&api.PullRequestPayload{ + Action: action, + Index: issue.Index, + PullRequest: convert.ToAPIPullRequest(ctx, issue.PullRequest, nil), + RequestedReviewer: convert.ToUser(ctx, reviewer, nil), + Repository: convert.ToRepo(ctx, issue.Repo, permission), + Sender: convert.ToUser(ctx, doer, nil), + }). + WithPullRequest(issue.PullRequest). + Notify(ctx) +} + func (*actionsNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { ctx = withMethod(ctx, "MergePullRequest") diff --git a/services/auth/source/db/source.go b/services/auth/source/db/source.go index 50eae27439..bb2270cbd6 100644 --- a/services/auth/source/db/source.go +++ b/services/auth/source/db/source.go @@ -18,7 +18,7 @@ func (source *Source) FromDB(bs []byte) error { return nil } -// ToDB exports an SMTPConfig to a serialized format. +// ToDB exports the config to a byte slice to be saved into database (this method is just dummy and does nothing for DB source) func (source *Source) ToDB() ([]byte, error) { return nil, nil } diff --git a/services/pull/commit_status.go b/services/pull/commit_status.go index 06e66fad77..27ee572640 100644 --- a/services/pull/commit_status.go +++ b/services/pull/commit_status.go @@ -52,6 +52,10 @@ func MergeRequiredContextsCommitStatus(commitStatuses []*git_model.CommitStatus, } } + if matchedCount != len(requiredContexts) { + return structs.CommitStatusPending + } + if matchedCount == 0 { status := git_model.CalcCommitStatus(commitStatuses) if status != nil { diff --git a/services/repository/push.go b/services/repository/push.go index 2ef8cac95e..5e2853b27d 100644 --- a/services/repository/push.go +++ b/services/repository/push.go @@ -321,14 +321,9 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo return nil } - lowerTags := make([]string, 0, len(tags)) - for _, tag := range tags { - lowerTags = append(lowerTags, strings.ToLower(tag)) - } - releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{ RepoID: repo.ID, - TagNames: lowerTags, + TagNames: tags, }) if err != nil { return fmt.Errorf("db.Find[repo_model.Release]: %w", err) @@ -338,6 +333,11 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo relMap[rel.LowerTagName] = rel } + lowerTags := make([]string, 0, len(tags)) + for _, tag := range tags { + lowerTags = append(lowerTags, strings.ToLower(tag)) + } + newReleases := make([]*repo_model.Release, 0, len(lowerTags)-len(relMap)) emailToUser := make(map[string]*user_model.User) diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 5cfd9ddefa..04f76748d0 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -88,7 +88,7 @@ {{ctx.Locale.Tr "packages.settings.delete"}}
- {{ctx.Locale.Tr "packages.settings.delete.notice" `` `` | Safe}} + {{ctx.Locale.Tr "packages.settings.delete.notice" (``|Safe) (``|Safe)}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index fdba0734a2..c7a6ec7e4e 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -101,7 +101,7 @@

{{ctx.Locale.Tr "repo.settings.delete_desc"}}

- {{ctx.Locale.Tr "repo.settings.delete_notices_2" `` | Safe}}
+ {{ctx.Locale.Tr "repo.settings.delete_notices_2" (``|Safe)}}
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index 894e41f8d7..aa5e810cd7 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -39,7 +39,7 @@ {{ctx.Locale.Tr "admin.monitor.process.cancel"}}
-

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" `` | Safe}}

+

{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (``|Safe)}}

{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/admin/user/new.tmpl b/templates/admin/user/new.tmpl index 81f70511d0..bcb53d8131 100644 --- a/templates/admin/user/new.tmpl +++ b/templates/admin/user/new.tmpl @@ -26,7 +26,7 @@
-

{{ctx.Locale.Tr "org.members.remove.detail" `` `` | Safe}}

+

{{ctx.Locale.Tr "org.members.remove.detail" (``|Safe) (``|Safe)}}

{{template "base/modal_actions_confirm" .}}
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl index da63d82967..dd4ece1433 100644 --- a/templates/org/team/members.tmpl +++ b/templates/org/team/members.tmpl @@ -81,7 +81,7 @@ {{ctx.Locale.Tr "org.members.remove"}}
-

{{ctx.Locale.Tr "org.members.remove.detail" `` `` | Safe}}

+

{{ctx.Locale.Tr "org.members.remove.detail" (``|Safe) (``|Safe)}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl index 29e7cf7cdd..37550ab71f 100644 --- a/templates/org/team/sidebar.tmpl +++ b/templates/org/team/sidebar.tmpl @@ -88,7 +88,7 @@ {{ctx.Locale.Tr "org.teams.leave"}}
-

{{ctx.Locale.Tr "org.teams.leave.detail" `` | Safe}}

+

{{ctx.Locale.Tr "org.teams.leave.detail" (``|Safe)}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl index f4ceada2a7..b518d7d9d7 100644 --- a/templates/org/team/teams.tmpl +++ b/templates/org/team/teams.tmpl @@ -49,7 +49,7 @@ {{ctx.Locale.Tr "org.teams.leave"}}
-

{{ctx.Locale.Tr "org.teams.leave.detail" `` | Safe}}

+

{{ctx.Locale.Tr "org.teams.leave.detail" (``|Safe)}}

{{template "base/modal_actions_confirm" .}} diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 01fa45babe..fbfaa19411 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -88,7 +88,7 @@ {{.CsrfTokenHtml}}
@@ -113,7 +113,7 @@
@@ -184,7 +184,7 @@
{{if .Commit.Signature}} -
+
{{if .Verification.Verified}} {{if ne .Verification.SigningUser.ID 0}} diff --git a/templates/repo/commit_statuses.tmpl b/templates/repo/commit_statuses.tmpl index ec2be6c38d..74c20a6a2c 100644 --- a/templates/repo/commit_statuses.tmpl +++ b/templates/repo/commit_statuses.tmpl @@ -1,10 +1,10 @@ {{if .Statuses}} {{if and (eq (len .Statuses) 1) .Status.TargetURL}} - + {{template "repo/commit_status" .Status}} {{else}} - + {{template "repo/commit_status" .Status}} {{end}} diff --git a/templates/repo/contributors.tmpl b/templates/repo/contributors.tmpl index 49a251c1f9..4a258e5b70 100644 --- a/templates/repo/contributors.tmpl +++ b/templates/repo/contributors.tmpl @@ -4,10 +4,10 @@ data-locale-contribution-type-commits="{{ctx.Locale.Tr "repo.contributors.contribution_type.commits"}}" data-locale-contribution-type-additions="{{ctx.Locale.Tr "repo.contributors.contribution_type.additions"}}" data-locale-contribution-type-deletions="{{ctx.Locale.Tr "repo.contributors.contribution_type.deletions"}}" - data-locale-loading-title="{{ctx.Locale.Tr "repo.contributors.loading_title"}}" - data-locale-loading-title-failed="{{ctx.Locale.Tr "repo.contributors.loading_title_failed"}}" - data-locale-loading-info="{{ctx.Locale.Tr "repo.contributors.loading_info"}}" - data-locale-component-failed-to-load="{{ctx.Locale.Tr "repo.contributors.component_failed_to_load"}}" + data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.contributors.what")}}" + data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.contributors.what")}}" + data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}" + data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}" >
{{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 05559fc9a7..9b4a9ba983 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -1,7 +1,7 @@ {{$showFileTree := (and (not .DiffNotAvailable) (gt .Diff.NumFiles 1))}}
-
+
{{if $showFileTree}}
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl index f1ab53eb67..e86deb8915 100644 --- a/templates/repo/issue/view_content/pull.tmpl +++ b/templates/repo/issue/view_content/pull.tmpl @@ -24,6 +24,7 @@ {{template "repo/pulls/status" (dict "CommitStatus" .LatestCommitStatus "CommitStatuses" .LatestCommitStatuses + "MissingRequiredChecks" .MissingRequiredChecks "ShowHideChecks" true "is_context_required" .is_context_required )}} @@ -38,7 +39,7 @@ {{ctx.Locale.Tr "repo.pulls.merged_success"}}
diff --git a/templates/repo/pulls/status.tmpl b/templates/repo/pulls/status.tmpl index ae508b8fa4..e8636ba1b8 100644 --- a/templates/repo/pulls/status.tmpl +++ b/templates/repo/pulls/status.tmpl @@ -2,6 +2,7 @@ Template Attributes: * CommitStatus: summary of all commit status state * CommitStatuses: all commit status elements +* MissingRequiredChecks: commit check contexts that are required by branch protection but not present * ShowHideChecks: whether use a button to show/hide the checks * is_context_required: Used in pull request commit status check table */}} @@ -9,7 +10,7 @@ Template Attributes: {{if .CommitStatus}}
- {{if eq .CommitStatus.State "pending"}} + {{if or (eq .CommitStatus.State "pending") (.MissingRequiredChecks)}} {{ctx.Locale.Tr "repo.pulls.status_checking"}} {{else if eq .CommitStatus.State "success"}} {{ctx.Locale.Tr "repo.pulls.status_checks_success"}} @@ -46,6 +47,13 @@ Template Attributes:
{{end}} + {{range .MissingRequiredChecks}} +
+ {{svg "octicon-dot-fill" 18 "commit-status icon text yellow"}} +
{{.}}
+
{{ctx.Locale.Tr "repo.pulls.status_checks_requested"}}
+
+ {{end}}
{{end}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index fb2fce2950..6dbeb741db 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -5,90 +5,90 @@ {{template "base/alert" .}} {{template "repo/release_tag_header" .}}
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index fefaa9dd17..e731cfe2e0 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -31,9 +31,8 @@
  • {{svg "octicon-location"}} {{.ContextUser.Location}} - {{if .UserLocationMapURL}} - {{/* We presume that the UserLocationMapURL is safe, as it is provided by the site administrator. */}} - + {{if .ContextUserLocationMapURL}} + {{svg "octicon-link-external"}} {{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 8a40cf76d4..0b330a89ee 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -19,9 +19,9 @@ "name": "MIT", "url": "http://opensource.org/licenses/MIT" }, - "version": "{{AppVer | JSEscape | Safe}}" + "version": "{{AppVer | JSEscape}}" }, - "basePath": "{{AppSubUrl | JSEscape | Safe}}/api/v1", + "basePath": "{{AppSubUrl | JSEscape}}/api/v1", "paths": { "/activitypub/user-id/{user-id}": { "get": { diff --git a/templates/user/auth/oidc_wellknown.tmpl b/templates/user/auth/oidc_wellknown.tmpl index 38e6900c38..54bb4a763d 100644 --- a/templates/user/auth/oidc_wellknown.tmpl +++ b/templates/user/auth/oidc_wellknown.tmpl @@ -1,16 +1,16 @@ { - "issuer": "{{AppUrl | JSEscape | Safe}}", - "authorization_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/authorize", - "token_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/access_token", - "jwks_uri": "{{AppUrl | JSEscape | Safe}}login/oauth/keys", - "userinfo_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/userinfo", - "introspection_endpoint": "{{AppUrl | JSEscape | Safe}}login/oauth/introspect", + "issuer": "{{AppUrl | JSEscape}}", + "authorization_endpoint": "{{AppUrl | JSEscape}}login/oauth/authorize", + "token_endpoint": "{{AppUrl | JSEscape}}login/oauth/access_token", + "jwks_uri": "{{AppUrl | JSEscape}}login/oauth/keys", + "userinfo_endpoint": "{{AppUrl | JSEscape}}login/oauth/userinfo", + "introspection_endpoint": "{{AppUrl | JSEscape}}login/oauth/introspect", "response_types_supported": [ "code", "id_token" ], "id_token_signing_alg_values_supported": [ - "{{.SigningKey.SigningMethod.Alg | JSEscape | Safe}}" + "{{.SigningKey.SigningMethod.Alg | JSEscape}}" ], "subject_types_supported": [ "public" diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl index 8079521984..102ff2e95b 100644 --- a/templates/user/settings/organization.tmpl +++ b/templates/user/settings/organization.tmpl @@ -47,7 +47,7 @@ {{ctx.Locale.Tr "org.members.leave"}}
  • -

    {{ctx.Locale.Tr "org.members.leave.detail" `` | Safe}}

    +

    {{ctx.Locale.Tr "org.members.leave.detail" (``|Safe)}}

    {{template "base/modal_actions_confirm" .}}
    diff --git a/web_src/css/base.css b/web_src/css/base.css index 0d547f16ff..76ecfc9bf5 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -21,6 +21,7 @@ --border-radius-circle: 50%; --opacity-disabled: 0.55; --height-loading: 16rem; + --min-height-textarea: 132px; /* padding + 6 lines + border = calc(1.57142em + 6lh + 2px), but lh is not fully supported */ --tab-size: 4; } @@ -492,6 +493,11 @@ ol.ui.list li, background: var(--color-active) !important; } +.ui.form textarea:not([rows]) { + height: var(--min-height-textarea); /* override fomantic default 12em */ + min-height: var(--min-height-textarea); /* override fomantic default 8em */ +} + /* styles from removed fomantic transition module */ .hidden.transition { visibility: hidden; diff --git a/web_src/css/editor/combomarkdowneditor.css b/web_src/css/editor/combomarkdowneditor.css index 12ec1866a5..8a2f4ea416 100644 --- a/web_src/css/editor/combomarkdowneditor.css +++ b/web_src/css/editor/combomarkdowneditor.css @@ -37,13 +37,12 @@ .combo-markdown-editor textarea.markdown-text-editor { display: block; width: 100%; - min-height: 200px; - max-height: calc(100vh - 200px); + max-height: calc(100vh - var(--min-height-textarea)); resize: vertical; } .combo-markdown-editor .CodeMirror-scroll { - max-height: calc(100vh - 200px); + max-height: calc(100vh - var(--min-height-textarea)); } /* use the same styles as markup/content.css */ diff --git a/web_src/css/repo.css b/web_src/css/repo.css index 610c3fcb55..31cff0ca15 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -1498,12 +1498,6 @@ background: var(--color-body); } -@media (max-width: 991.98px) { - .repository .diff-detail-box { - flex-direction: row; - } -} - @media (max-width: 480px) { .repository .diff-detail-box { flex-wrap: wrap; @@ -1528,7 +1522,7 @@ color: var(--color-red); } -@media (max-width: 991.98px) { +@media (max-width: 800px) { .repository .diff-detail-box .diff-detail-stats { display: none !important; } @@ -1538,7 +1532,6 @@ display: flex; align-items: center; gap: 0.25em; - flex-wrap: wrap; justify-content: end; } @@ -1548,15 +1541,6 @@ margin-right: 0 !important; } -@media (max-width: 480px) { - .repository .diff-detail-box .diff-detail-actions { - padding-top: 0.25rem; - } - .repository .diff-detail-box .diff-detail-actions .ui.button:not(.btn-submit) { - padding: 0 0.5rem; - } -} - .repository .diff-detail-box span.status { display: inline-block; width: 12px; diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js index 27abcfe56f..444ab89150 100644 --- a/web_src/js/features/comp/ImagePaste.js +++ b/web_src/js/features/comp/ImagePaste.js @@ -1,5 +1,7 @@ import $ from 'jquery'; +import {htmlEscape} from 'escape-goat'; import {POST} from '../../modules/fetch.js'; +import {imageInfo} from '../../utils/image.js'; async function uploadFile(file, uploadUrl) { const formData = new FormData(); @@ -109,10 +111,22 @@ const uploadClipboardImage = async (editor, dropzone, e) => { const placeholder = `![${name}](uploading ...)`; editor.insertPlaceholder(placeholder); - const data = await uploadFile(img, uploadUrl); - editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`); - const $input = $(``).attr('id', data.uuid).val(data.uuid); + const {uuid} = await uploadFile(img, uploadUrl); + const {width, dppx} = await imageInfo(img); + + const url = `/attachments/${uuid}`; + let text; + if (width > 0 && dppx > 1) { + // Scale down images from HiDPI monitors. This uses the tag because it's the only + // method to change image size in Markdown that is supported by all implementations. + text = `${htmlEscape(name)}`; + } else { + text = `![${name}](${url})`; + } + editor.replacePlaceholder(placeholder, text); + + const $input = $(``).attr('id', uuid).val(uuid); $files.append($input); } }; diff --git a/web_src/js/features/repo-migrate.js b/web_src/js/features/repo-migrate.js index cae28fdd1b..490e7df0e4 100644 --- a/web_src/js/features/repo-migrate.js +++ b/web_src/js/features/repo-migrate.js @@ -1,18 +1,17 @@ -import $ from 'jquery'; import {hideElem, showElem} from '../utils/dom.js'; import {GET, POST} from '../modules/fetch.js'; const {appSubUrl} = window.config; export function initRepoMigrationStatusChecker() { - const $repoMigrating = $('#repo_migrating'); - if (!$repoMigrating.length) return; + const repoMigrating = document.getElementById('repo_migrating'); + if (!repoMigrating) return; - $('#repo_migrating_retry').on('click', doMigrationRetry); + document.getElementById('repo_migrating_retry').addEventListener('click', doMigrationRetry); - const task = $repoMigrating.attr('data-migrating-task-id'); + const task = repoMigrating.getAttribute('data-migrating-task-id'); - // returns true if the refresh still need to be called after a while + // returns true if the refresh still needs to be called after a while const refresh = async () => { const res = await GET(`${appSubUrl}/user/task/${task}`); if (res.status !== 200) return true; // continue to refresh if network error occurs @@ -21,7 +20,7 @@ export function initRepoMigrationStatusChecker() { // for all status if (data.message) { - $('#repo_migrating_progress_message').text(data.message); + document.getElementById('repo_migrating_progress_message').textContent = data.message; } // TaskStatusFinished @@ -37,7 +36,7 @@ export function initRepoMigrationStatusChecker() { showElem('#repo_migrating_retry'); showElem('#repo_migrating_failed'); showElem('#repo_migrating_failed_image'); - $('#repo_migrating_failed_error').text(data.message); + document.getElementById('repo_migrating_failed_error').textContent = data.message; return false; } @@ -59,6 +58,6 @@ export function initRepoMigrationStatusChecker() { } async function doMigrationRetry(e) { - await POST($(e.target).attr('data-migrating-task-retry-url')); + await POST(e.target.getAttribute('data-migrating-task-retry-url')); window.location.reload(); } diff --git a/web_src/js/features/user-auth.js b/web_src/js/features/user-auth.js index af380dcfc7..60d186e699 100644 --- a/web_src/js/features/user-auth.js +++ b/web_src/js/features/user-auth.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import {checkAppUrl} from './common-global.js'; export function initUserAuthOauth2() { @@ -21,30 +20,3 @@ export function initUserAuthOauth2() { }); } } - -export function initUserAuthLinkAccountView() { - const $lnkUserPage = $('.page-content.user.link-account'); - if ($lnkUserPage.length === 0) { - return false; - } - - const $signinTab = $lnkUserPage.find('.item[data-tab="auth-link-signin-tab"]'); - const $signUpTab = $lnkUserPage.find('.item[data-tab="auth-link-signup-tab"]'); - const $signInView = $lnkUserPage.find('.tab[data-tab="auth-link-signin-tab"]'); - const $signUpView = $lnkUserPage.find('.tab[data-tab="auth-link-signup-tab"]'); - - $signUpTab.on('click', () => { - $signinTab.removeClass('active'); - $signInView.removeClass('active'); - $signUpTab.addClass('active'); - $signUpView.addClass('active'); - return false; - }); - - $signinTab.on('click', () => { - $signUpTab.removeClass('active'); - $signUpView.removeClass('active'); - $signinTab.addClass('active'); - $signInView.addClass('active'); - }); -} diff --git a/web_src/js/index.js b/web_src/js/index.js index 078f9fc9df..117279c3c4 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -23,7 +23,7 @@ import {initFindFileInRepo} from './features/repo-findfile.js'; import {initCommentContent, initMarkupContent} from './markup/content.js'; import {initPdfViewer} from './render/pdf.js'; -import {initUserAuthLinkAccountView, initUserAuthOauth2} from './features/user-auth.js'; +import {initUserAuthOauth2} from './features/user-auth.js'; import { initRepoIssueDue, initRepoIssueReferenceRepositorySearch, @@ -178,7 +178,6 @@ onDomReady(() => { initCommitStatuses(); initCaptcha(); - initUserAuthLinkAccountView(); initUserAuthOauth2(); initUserAuthWebAuthn(); initUserAuthWebAuthnRegister(); diff --git a/web_src/js/utils/image.js b/web_src/js/utils/image.js new file mode 100644 index 0000000000..ed5d98e35a --- /dev/null +++ b/web_src/js/utils/image.js @@ -0,0 +1,47 @@ +export async function pngChunks(blob) { + const uint8arr = new Uint8Array(await blob.arrayBuffer()); + const chunks = []; + if (uint8arr.length < 12) return chunks; + const view = new DataView(uint8arr.buffer); + if (view.getBigUint64(0) !== 9894494448401390090n) return chunks; + + const decoder = new TextDecoder(); + let index = 8; + while (index < uint8arr.length) { + const len = view.getUint32(index); + chunks.push({ + name: decoder.decode(uint8arr.slice(index + 4, index + 8)), + data: uint8arr.slice(index + 8, index + 8 + len), + }); + index += len + 12; + } + + return chunks; +} + +// decode a image and try to obtain width and dppx. If will never throw but instead +// return default values. +export async function imageInfo(blob) { + let width = 0; // 0 means no width could be determined + let dppx = 1; // 1 dot per pixel for non-HiDPI screens + + if (blob.type === 'image/png') { // only png is supported currently + try { + for (const {name, data} of await pngChunks(blob)) { + const view = new DataView(data.buffer); + if (name === 'IHDR' && data?.length) { + // extract width from mandatory IHDR chunk + width = view.getUint32(0); + } else if (name === 'pHYs' && data?.length) { + // extract dppx from optional pHYs chunk, assuming pixels are square + const unit = view.getUint8(8); + if (unit === 1) { + dppx = Math.round(view.getUint32(0) / 39.3701) / 72; // meter to inch to dppx + } + } + } + } catch {} + } + + return {width, dppx}; +} diff --git a/web_src/js/utils/image.test.js b/web_src/js/utils/image.test.js new file mode 100644 index 0000000000..ba4758250c --- /dev/null +++ b/web_src/js/utils/image.test.js @@ -0,0 +1,29 @@ +import {pngChunks, imageInfo} from './image.js'; + +const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAADUlEQVQIHQECAP3/AAAAAgABzePRKwAAAABJRU5ErkJggg=='; +const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; +const pngEmpty = 'data:image/png;base64,'; + +async function dataUriToBlob(datauri) { + return await (await globalThis.fetch(datauri)).blob(); +} + +test('pngChunks', async () => { + expect(await pngChunks(await dataUriToBlob(pngNoPhys))).toEqual([ + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0])}, + {name: 'IDAT', data: new Uint8Array([8, 29, 1, 2, 0, 253, 255, 0, 0, 0, 2, 0, 1])}, + {name: 'IEND', data: new Uint8Array([])}, + ]); + expect(await pngChunks(await dataUriToBlob(pngPhys))).toEqual([ + {name: 'IHDR', data: new Uint8Array([0, 0, 0, 2, 0, 0, 0, 2, 8, 2, 0, 0, 0])}, + {name: 'pHYs', data: new Uint8Array([0, 0, 22, 37, 0, 0, 22, 37, 1])}, + {name: 'IDAT', data: new Uint8Array([8, 215, 99, 144, 53, 151, 0, 34, 6, 8, 5, 0, 11, 242, 1, 177])}, + ]); + expect(await pngChunks(await dataUriToBlob(pngEmpty))).toEqual([]); +}); + +test('imageInfo', async () => { + expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1}); + expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2}); + expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1}); +});