##// END OF EJS Templates
controller: Handle UnicodeDecodeError from webob decoding invalid URLs...
Valentin Kleibel -
r8790:aa51aca7 stable
parent child Browse files
Show More
@@ -1,197 +1,198
1 List of contributors to Kallithea project:
1 List of contributors to Kallithea project:
2
2
3 Mads Kiilerich <mads@kiilerich.com> 2016-2024
3 Mads Kiilerich <mads@kiilerich.com> 2016-2024
4 Aristotelis Stageiritis <aristotelis79@gmail.com> 2024
4 Aristotelis Stageiritis <aristotelis79@gmail.com> 2024
5 Poesty Li <poesty7450@gmail.com> 2024
5 Poesty Li <poesty7450@gmail.com> 2024
6 Valentin Kleibel <valentin@vrvis.at> 2024
6 Manuel Jacob <me@manueljacob.de> 2019-2020 2022-2023
7 Manuel Jacob <me@manueljacob.de> 2019-2020 2022-2023
7 Mathias De Mare <mathias.de_mare@nokia.com> 2023
8 Mathias De Mare <mathias.de_mare@nokia.com> 2023
8 qy117121 <mixuan121@gmail.com> 2023
9 qy117121 <mixuan121@gmail.com> 2023
9 Asterios Dimitriou <steve@pci.gr> 2016-2017 2020 2022
10 Asterios Dimitriou <steve@pci.gr> 2016-2017 2020 2022
10 Γ‰tienne Gilli <etienne@gilli.io> 2020-2022
11 Γ‰tienne Gilli <etienne@gilli.io> 2020-2022
11 Jaime MarquΓ­nez FerrΓ‘ndiz <weblate@jregistros.fastmail.net> 2022
12 Jaime MarquΓ­nez FerrΓ‘ndiz <weblate@jregistros.fastmail.net> 2022
12 Louis Bertrand <louis.bertrand@durhamcollege.ca> 2022
13 Louis Bertrand <louis.bertrand@durhamcollege.ca> 2022
13 toras9000 <toras9000@gmail.com> 2022
14 toras9000 <toras9000@gmail.com> 2022
14 yzqzss <yzqzss@othing.xyz> 2022
15 yzqzss <yzqzss@othing.xyz> 2022
15 МАН69К <weblate@mah69k.net> 2022
16 МАН69К <weblate@mah69k.net> 2022
16 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2021
17 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2021
17 ssantos <ssantos@web.de> 2018-2021
18 ssantos <ssantos@web.de> 2018-2021
18 Private <adamantine.sword@gmail.com> 2019-2021
19 Private <adamantine.sword@gmail.com> 2019-2021
19 fresh <fresh190@protonmail.com> 2020-2021
20 fresh <fresh190@protonmail.com> 2020-2021
20 robertus <robertuss12@gmail.com> 2020-2021
21 robertus <robertuss12@gmail.com> 2020-2021
21 Eugenia Russell <eugenia.russell2019@gmail.com> 2021
22 Eugenia Russell <eugenia.russell2019@gmail.com> 2021
22 Michalis <michalisntovas@yahoo.gr> 2021
23 Michalis <michalisntovas@yahoo.gr> 2021
23 vs <vsuhachev@yandex.ru> 2021
24 vs <vsuhachev@yandex.ru> 2021
24 АлСксандр <akonn7@mail.ru> 2021
25 АлСксандр <akonn7@mail.ru> 2021
25 Allan NordhΓΈy <epost@anotheragency.no> 2017-2020
26 Allan NordhΓΈy <epost@anotheragency.no> 2017-2020
26 Anton Schur <tonich.sh@gmail.com> 2017 2020
27 Anton Schur <tonich.sh@gmail.com> 2017 2020
27 Artem <kovalevartem.ru@gmail.com> 2020
28 Artem <kovalevartem.ru@gmail.com> 2020
28 David Ignjić <ignjic@gmail.com> 2020
29 David Ignjić <ignjic@gmail.com> 2020
29 Dennis Fink <dennis.fink@c3l.lu> 2020
30 Dennis Fink <dennis.fink@c3l.lu> 2020
30 J. Lavoie <j.lavoie@net-c.ca> 2020
31 J. Lavoie <j.lavoie@net-c.ca> 2020
31 Ross Thomas <ross@lns-nevasoft.com> 2020
32 Ross Thomas <ross@lns-nevasoft.com> 2020
32 Tim Ooms <tatankat@users.noreply.github.com> 2020
33 Tim Ooms <tatankat@users.noreply.github.com> 2020
33 Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019
34 Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019
34 Γ‰tienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019
35 Γ‰tienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019
35 Adi Kriegisch <adi@cg.tuwien.ac.at> 2019
36 Adi Kriegisch <adi@cg.tuwien.ac.at> 2019
36 Danni Randeris <danniranderis@gmail.com> 2019
37 Danni Randeris <danniranderis@gmail.com> 2019
37 Edmund Wong <ewong@crazy-cat.org> 2019
38 Edmund Wong <ewong@crazy-cat.org> 2019
38 Elizabeth Sherrock <lizzyd710@gmail.com> 2019
39 Elizabeth Sherrock <lizzyd710@gmail.com> 2019
39 Hüseyin Tunç <huseyin.tunc@bulutfon.com> 2019
40 Hüseyin Tunç <huseyin.tunc@bulutfon.com> 2019
40 leela <53352@protonmail.com> 2019
41 leela <53352@protonmail.com> 2019
41 Mateusz Mendel <mendelm9@gmail.com> 2019
42 Mateusz Mendel <mendelm9@gmail.com> 2019
42 Nathan <bonnemainsnathan@gmail.com> 2019
43 Nathan <bonnemainsnathan@gmail.com> 2019
43 Oleksandr Shtalinberg <o.shtalinberg@gmail.com> 2019
44 Oleksandr Shtalinberg <o.shtalinberg@gmail.com> 2019
44 THANOS SIOURDAKIS <siourdakisthanos@gmail.com> 2019
45 THANOS SIOURDAKIS <siourdakisthanos@gmail.com> 2019
45 Wolfgang Scherer <wolfgang.scherer@gmx.de> 2019
46 Wolfgang Scherer <wolfgang.scherer@gmx.de> 2019
46 Π₯ристо Π‘Ρ‚Π°Π½Π΅Π² <hstanev@gmail.com> 2019
47 Π₯ристо Π‘Ρ‚Π°Π½Π΅Π² <hstanev@gmail.com> 2019
47 Dominik Ruf <dominikruf@gmail.com> 2012 2014-2018
48 Dominik Ruf <dominikruf@gmail.com> 2012 2014-2018
48 Michal ČihaΕ™ <michal@cihar.com> 2014-2015 2018
49 Michal ČihaΕ™ <michal@cihar.com> 2014-2015 2018
49 Branko Majic <branko@majic.rs> 2015 2018
50 Branko Majic <branko@majic.rs> 2015 2018
50 Chris Rule <crule@aegistg.com> 2018
51 Chris Rule <crule@aegistg.com> 2018
51 JesΓΊs SΓ‘nchez <jsanchezfdz95@gmail.com> 2018
52 JesΓΊs SΓ‘nchez <jsanchezfdz95@gmail.com> 2018
52 Patrick Vane <patrick_vane@lowentry.com> 2018
53 Patrick Vane <patrick_vane@lowentry.com> 2018
53 Pheng Heong Tan <phtan90@gmail.com> 2018
54 Pheng Heong Tan <phtan90@gmail.com> 2018
54 Максим Π―ΠΊΠΈΠΌΡ‡ΡƒΠΊ <xpinovo@gmail.com> 2018
55 Максим Π―ΠΊΠΈΠΌΡ‡ΡƒΠΊ <xpinovo@gmail.com> 2018
55 ΠœΠ°Ρ€Ρ Π―ΠΌΠ±Π°Ρ€ <mjambarmeta@gmail.com> 2018
56 ΠœΠ°Ρ€Ρ Π―ΠΌΠ±Π°Ρ€ <mjambarmeta@gmail.com> 2018
56 Mads Kiilerich <madski@unity3d.com> 2012-2017
57 Mads Kiilerich <madski@unity3d.com> 2012-2017
57 Unity Technologies 2012-2017
58 Unity Technologies 2012-2017
58 SΓΈren LΓΈvborg <sorenl@unity3d.com> 2015-2017
59 SΓΈren LΓΈvborg <sorenl@unity3d.com> 2015-2017
59 Sam Jaques <sam.jaques@me.com> 2015 2017
60 Sam Jaques <sam.jaques@me.com> 2015 2017
60 Alessandro Molina <alessandro.molina@axant.it> 2017
61 Alessandro Molina <alessandro.molina@axant.it> 2017
61 Ching-Chen Mao <mao@lins.fju.edu.tw> 2017
62 Ching-Chen Mao <mao@lins.fju.edu.tw> 2017
62 Eivind Tagseth <eivindt@gmail.com> 2017
63 Eivind Tagseth <eivindt@gmail.com> 2017
63 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> 2017
64 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> 2017
64 Holger Schramm <info@schramm.by> 2017
65 Holger Schramm <info@schramm.by> 2017
65 Karl Goetz <karl@kgoetz.id.au> 2017
66 Karl Goetz <karl@kgoetz.id.au> 2017
66 Lars Kruse <devel@sumpfralle.de> 2017
67 Lars Kruse <devel@sumpfralle.de> 2017
67 Marko Semet <markosemet@googlemail.com> 2017
68 Marko Semet <markosemet@googlemail.com> 2017
68 Viktar Vauchkevich <victorenator@gmail.com> 2017
69 Viktar Vauchkevich <victorenator@gmail.com> 2017
69 Takumi IINO <trot.thunder@gmail.com> 2012-2016
70 Takumi IINO <trot.thunder@gmail.com> 2012-2016
70 Jan Heylen <heyleke@gmail.com> 2015-2016
71 Jan Heylen <heyleke@gmail.com> 2015-2016
71 Robert Martinez <ntttq@inboxen.org> 2015-2016
72 Robert Martinez <ntttq@inboxen.org> 2015-2016
72 Robert Rauch <mail@robertrauch.de> 2015-2016
73 Robert Rauch <mail@robertrauch.de> 2015-2016
73 Angel Ezquerra <angel.ezquerra@gmail.com> 2016
74 Angel Ezquerra <angel.ezquerra@gmail.com> 2016
74 Anton Shestakov <av6@dwimlabs.net> 2016
75 Anton Shestakov <av6@dwimlabs.net> 2016
75 Brandon Jones <bjones14@gmail.com> 2016
76 Brandon Jones <bjones14@gmail.com> 2016
76 Kateryna Musina <kateryna@unity3d.com> 2016
77 Kateryna Musina <kateryna@unity3d.com> 2016
77 Konstantin Veretennicov <kveretennicov@gmail.com> 2016
78 Konstantin Veretennicov <kveretennicov@gmail.com> 2016
78 Oscar Curero <oscar@naiandei.net> 2016
79 Oscar Curero <oscar@naiandei.net> 2016
79 Robert James Dennington <tinytimrob@googlemail.com> 2016
80 Robert James Dennington <tinytimrob@googlemail.com> 2016
80 timeless@gmail.com 2016
81 timeless@gmail.com 2016
81 YFdyh000 <yfdyh000@gmail.com> 2016
82 YFdyh000 <yfdyh000@gmail.com> 2016
82 Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
83 Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
83 Sean Farley <sean.michael.farley@gmail.com> 2013-2015
84 Sean Farley <sean.michael.farley@gmail.com> 2013-2015
84 Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014-2015
85 Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014-2015
85 Christian Oyarzun <oyarzun@gmail.com> 2014-2015
86 Christian Oyarzun <oyarzun@gmail.com> 2014-2015
86 Joseph Rivera <rivera.d.joseph@gmail.com> 2014-2015
87 Joseph Rivera <rivera.d.joseph@gmail.com> 2014-2015
87 Anatoly Bubenkov <bubenkoff@gmail.com> 2015
88 Anatoly Bubenkov <bubenkoff@gmail.com> 2015
88 Andrew Bartlett <abartlet@catalyst.net.nz> 2015
89 Andrew Bartlett <abartlet@catalyst.net.nz> 2015
89 BalÑzs Úr <urbalazs@gmail.com> 2015
90 BalÑzs Úr <urbalazs@gmail.com> 2015
90 Ben Finney <ben@benfinney.id.au> 2015
91 Ben Finney <ben@benfinney.id.au> 2015
91 Daniel Hobley <danielh@unity3d.com> 2015
92 Daniel Hobley <danielh@unity3d.com> 2015
92 David Avigni <david.avigni@ankapi.com> 2015
93 David Avigni <david.avigni@ankapi.com> 2015
93 Denis Blanchette <dblanchette@coveo.com> 2015
94 Denis Blanchette <dblanchette@coveo.com> 2015
94 duanhongyi <duanhongyi@doopai.com> 2015
95 duanhongyi <duanhongyi@doopai.com> 2015
95 EriCSN Chang <ericsning@gmail.com> 2015
96 EriCSN Chang <ericsning@gmail.com> 2015
96 Grzegorz Krason <grzegorz.krason@gmail.com> 2015
97 Grzegorz Krason <grzegorz.krason@gmail.com> 2015
97 JiΕ™Γ­ Suchan <yed@vanyli.net> 2015
98 JiΕ™Γ­ Suchan <yed@vanyli.net> 2015
98 Kazunari Kobayashi <kobanari@nifty.com> 2015
99 Kazunari Kobayashi <kobanari@nifty.com> 2015
99 Kevin Bullock <kbullock@ringworld.org> 2015
100 Kevin Bullock <kbullock@ringworld.org> 2015
100 kobanari <kobanari@nifty.com> 2015
101 kobanari <kobanari@nifty.com> 2015
101 Marc Abramowitz <marc@marc-abramowitz.com> 2015
102 Marc Abramowitz <marc@marc-abramowitz.com> 2015
102 Marc Villetard <marc.villetard@gmail.com> 2015
103 Marc Villetard <marc.villetard@gmail.com> 2015
103 Matthias Zilk <matthias.zilk@gmail.com> 2015
104 Matthias Zilk <matthias.zilk@gmail.com> 2015
104 Michael Pohl <michael@mipapo.de> 2015
105 Michael Pohl <michael@mipapo.de> 2015
105 Michael V. DePalatis <mike@depalatis.net> 2015
106 Michael V. DePalatis <mike@depalatis.net> 2015
106 Morten Skaaning <mortens@unity3d.com> 2015
107 Morten Skaaning <mortens@unity3d.com> 2015
107 Nick High <nick@silverchip.org> 2015
108 Nick High <nick@silverchip.org> 2015
108 Niemand Jedermann <predatorix@web.de> 2015
109 Niemand Jedermann <predatorix@web.de> 2015
109 Peter Vitt <petervitt@web.de> 2015
110 Peter Vitt <petervitt@web.de> 2015
110 Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
111 Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
111 Tuux <tuxa@galaxie.eu.org> 2015
112 Tuux <tuxa@galaxie.eu.org> 2015
112 Viktar Palstsiuk <vipals@gmail.com> 2015
113 Viktar Palstsiuk <vipals@gmail.com> 2015
113 Ante Ilic <ante@unity3d.com> 2014
114 Ante Ilic <ante@unity3d.com> 2014
114 Calinou <calinou@opmbx.org> 2014
115 Calinou <calinou@opmbx.org> 2014
115 Daniel Anderson <daniel@dattrix.com> 2014
116 Daniel Anderson <daniel@dattrix.com> 2014
116 Henrik Stuart <hg@hstuart.dk> 2014
117 Henrik Stuart <hg@hstuart.dk> 2014
117 Ingo von Borstel <kallithea@planetmaker.de> 2014
118 Ingo von Borstel <kallithea@planetmaker.de> 2014
118 invision70 <invision70@gmail.com> 2014
119 invision70 <invision70@gmail.com> 2014
119 Jelmer VernooΔ³ <jelmer@samba.org> 2014
120 Jelmer VernooΔ³ <jelmer@samba.org> 2014
120 Jim Hague <jim.hague@acm.org> 2014
121 Jim Hague <jim.hague@acm.org> 2014
121 Matt Fellows <kallithea@matt-fellows.me.uk> 2014
122 Matt Fellows <kallithea@matt-fellows.me.uk> 2014
122 Max Roman <max@choloclos.se> 2014
123 Max Roman <max@choloclos.se> 2014
123 Na'Tosha Bard <natosha@unity3d.com> 2014
124 Na'Tosha Bard <natosha@unity3d.com> 2014
124 Rasmus Selsmark <rasmuss@unity3d.com> 2014
125 Rasmus Selsmark <rasmuss@unity3d.com> 2014
125 SkryabinD <skryabind@gmail.com> 2014
126 SkryabinD <skryabind@gmail.com> 2014
126 Tim Freund <tim@freunds.net> 2014
127 Tim Freund <tim@freunds.net> 2014
127 Travis Burtrum <android@moparisthebest.com> 2014
128 Travis Burtrum <android@moparisthebest.com> 2014
128 whosaysni <whosaysni@gmail.com> 2014
129 whosaysni <whosaysni@gmail.com> 2014
129 Zoltan Gyarmati <mr.zoltan.gyarmati@gmail.com> 2014
130 Zoltan Gyarmati <mr.zoltan.gyarmati@gmail.com> 2014
130 Marcin KuΕΊmiΕ„ski <marcin@python-works.com> 2010-2013
131 Marcin KuΕΊmiΕ„ski <marcin@python-works.com> 2010-2013
131 Nemcio <areczek01@gmail.com> 2012-2013
132 Nemcio <areczek01@gmail.com> 2012-2013
132 xpol <xpolife@gmail.com> 2012-2013
133 xpol <xpolife@gmail.com> 2012-2013
133 Andrey Mivrenik <myvrenik@gmail.com> 2013
134 Andrey Mivrenik <myvrenik@gmail.com> 2013
134 Aparkar <aparkar@icloud.com> 2013
135 Aparkar <aparkar@icloud.com> 2013
135 ArcheR <aleclitvinov1980@gmail.com> 2013
136 ArcheR <aleclitvinov1980@gmail.com> 2013
136 Dennis Brakhane <brakhane@googlemail.com> 2013
137 Dennis Brakhane <brakhane@googlemail.com> 2013
137 gnustavo <gustavo@gnustavo.com> 2013
138 gnustavo <gustavo@gnustavo.com> 2013
138 Grzegorz RoΕΌniecki <xaerxess@gmail.com> 2013
139 Grzegorz RoΕΌniecki <xaerxess@gmail.com> 2013
139 Ilya Beda <ir4y.ix@gmail.com> 2013
140 Ilya Beda <ir4y.ix@gmail.com> 2013
140 ivlevdenis <ivlevdenis.ru@gmail.com> 2013
141 ivlevdenis <ivlevdenis.ru@gmail.com> 2013
141 Jonathan Sternberg <jonathansternberg@gmail.com> 2013
142 Jonathan Sternberg <jonathansternberg@gmail.com> 2013
142 Leonardo Carneiro <leonardo@unity3d.com> 2013
143 Leonardo Carneiro <leonardo@unity3d.com> 2013
143 Magnus Ericmats <magnus.ericmats@gmail.com> 2013
144 Magnus Ericmats <magnus.ericmats@gmail.com> 2013
144 Martin Vium <martinv@unity3d.com> 2013
145 Martin Vium <martinv@unity3d.com> 2013
145 Mikhail Zholobov <legal90@gmail.com> 2013
146 Mikhail Zholobov <legal90@gmail.com> 2013
146 mokeev1995 <mokeev_andre@mail.ru> 2013
147 mokeev1995 <mokeev_andre@mail.ru> 2013
147 Ruslan Bekenev <furyinbox@gmail.com> 2013
148 Ruslan Bekenev <furyinbox@gmail.com> 2013
148 shirou - しろう 2013
149 shirou - しろう 2013
149 Simon Lopez <simon.lopez@slopez.org> 2013
150 Simon Lopez <simon.lopez@slopez.org> 2013
150 softforwinxp <softforwinxp@gmail.com> 2013
151 softforwinxp <softforwinxp@gmail.com> 2013
151 stephanj <info@stephan-jauernick.de> 2013
152 stephanj <info@stephan-jauernick.de> 2013
152 Ton Plomp <tcplomp@gmail.com> 2013
153 Ton Plomp <tcplomp@gmail.com> 2013
153 zhmylove <zhmylove@narod.ru> 2013
154 zhmylove <zhmylove@narod.ru> 2013
154 こいんとす <tkondou@gmail.com> 2013
155 こいんとす <tkondou@gmail.com> 2013
155 Augusto Herrmann <augusto.herrmann@planejamento.gov.br> 2011-2012
156 Augusto Herrmann <augusto.herrmann@planejamento.gov.br> 2011-2012
156 Augusto Herrmann <augusto.herrmann@gmail.com> 2012
157 Augusto Herrmann <augusto.herrmann@gmail.com> 2012
157 Dan Sheridan <djs@adelard.com> 2012
158 Dan Sheridan <djs@adelard.com> 2012
158 Dies Koper <diesk@fast.au.fujitsu.com> 2012
159 Dies Koper <diesk@fast.au.fujitsu.com> 2012
159 Erwin Kroon <e.kroon@smartmetersolutions.nl> 2012
160 Erwin Kroon <e.kroon@smartmetersolutions.nl> 2012
160 H Waldo G <gwaldo@gmail.com> 2012
161 H Waldo G <gwaldo@gmail.com> 2012
161 hppj <hppj@postmage.biz> 2012
162 hppj <hppj@postmage.biz> 2012
162 Indra Talip <indra.talip@gmail.com> 2012
163 Indra Talip <indra.talip@gmail.com> 2012
163 mikespook <mikespook@gmail.com> 2012
164 mikespook <mikespook@gmail.com> 2012
164 nansenat16 <nansenat16@null.tw> 2012
165 nansenat16 <nansenat16@null.tw> 2012
165 Nemcio <bogdan114@g.pl> 2012
166 Nemcio <bogdan114@g.pl> 2012
166 Philip Jameson <philip.j@hostdime.com> 2012
167 Philip Jameson <philip.j@hostdime.com> 2012
167 Raoul Thill <raoul.thill@gmail.com> 2012
168 Raoul Thill <raoul.thill@gmail.com> 2012
168 Stefan Engel <mail@engel-stefan.de> 2012
169 Stefan Engel <mail@engel-stefan.de> 2012
169 Tony Bussieres <t.bussieres@gmail.com> 2012
170 Tony Bussieres <t.bussieres@gmail.com> 2012
170 Vincent Caron <vcaron@bearstech.com> 2012
171 Vincent Caron <vcaron@bearstech.com> 2012
171 Vincent Duvert <vincent@duvert.net> 2012
172 Vincent Duvert <vincent@duvert.net> 2012
172 Vladislav Poluhin <nuklea@gmail.com> 2012
173 Vladislav Poluhin <nuklea@gmail.com> 2012
173 Zachary Auclair <zach101@gmail.com> 2012
174 Zachary Auclair <zach101@gmail.com> 2012
174 Ankit Solanki <ankit.solanki@gmail.com> 2011
175 Ankit Solanki <ankit.solanki@gmail.com> 2011
175 Dmitri Kuznetsov 2011
176 Dmitri Kuznetsov 2011
176 Jared Bunting <jared.bunting@peachjean.com> 2011
177 Jared Bunting <jared.bunting@peachjean.com> 2011
177 Jason Harris <jason@jasonfharris.com> 2011
178 Jason Harris <jason@jasonfharris.com> 2011
178 Les Peabody <lpeabody@gmail.com> 2011
179 Les Peabody <lpeabody@gmail.com> 2011
179 Liad Shani <liadff@gmail.com> 2011
180 Liad Shani <liadff@gmail.com> 2011
180 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it> 2011
181 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it> 2011
181 Matt Zuba <matt.zuba@goodwillaz.org> 2011
182 Matt Zuba <matt.zuba@goodwillaz.org> 2011
182 Nicolas VINOT <aeris@imirhil.fr> 2011
183 Nicolas VINOT <aeris@imirhil.fr> 2011
183 Shawn K. O'Shea <shawn@eth0.net> 2011
184 Shawn K. O'Shea <shawn@eth0.net> 2011
184 Thayne Harbaugh <thayne@fusionio.com> 2011
185 Thayne Harbaugh <thayne@fusionio.com> 2011
185 Łukasz Balcerzak <lukaszbalcerzak@gmail.com> 2010
186 Łukasz Balcerzak <lukaszbalcerzak@gmail.com> 2010
186 Andrew Kesterson <andrew@aklabs.net>
187 Andrew Kesterson <andrew@aklabs.net>
187 cejones
188 cejones
188 David A. SjΓΈen <david.sjoen@westcon.no>
189 David A. SjΓΈen <david.sjoen@westcon.no>
189 James Rhodes <jrhodes@redpointsoftware.com.au>
190 James Rhodes <jrhodes@redpointsoftware.com.au>
190 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
191 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
191 larikale
192 larikale
192 RhodeCode GmbH
193 RhodeCode GmbH
193 Sebastian Kreutzberger <sebastian@rhodecode.com>
194 Sebastian Kreutzberger <sebastian@rhodecode.com>
194 Steve Romanow <slestak989@gmail.com>
195 Steve Romanow <slestak989@gmail.com>
195 SteveCohen
196 SteveCohen
196 Thomas <thomas@rhodecode.com>
197 Thomas <thomas@rhodecode.com>
197 Thomas Waldmann <tw-public@gmx.de>
198 Thomas Waldmann <tw-public@gmx.de>
@@ -1,633 +1,641
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 """
15 """
16 kallithea.controllers.base
16 kallithea.controllers.base
17 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17 ~~~~~~~~~~~~~~~~~~~~~~~~~~
18
18
19 The base Controller API
19 The base Controller API
20 Provides the BaseController class for subclassing. And usage in different
20 Provides the BaseController class for subclassing. And usage in different
21 controllers
21 controllers
22
22
23 This file was forked by the Kallithea project in July 2014.
23 This file was forked by the Kallithea project in July 2014.
24 Original author and date, and relevant copyright and licensing information is below:
24 Original author and date, and relevant copyright and licensing information is below:
25 :created_on: Oct 06, 2010
25 :created_on: Oct 06, 2010
26 :author: marcink
26 :author: marcink
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 :license: GPLv3, see LICENSE.md for more details.
28 :license: GPLv3, see LICENSE.md for more details.
29 """
29 """
30
30
31 import base64
31 import base64
32 import datetime
32 import datetime
33 import logging
33 import logging
34 import traceback
34 import traceback
35 import warnings
35 import warnings
36
36
37 import decorator
37 import decorator
38 import paste.auth.basic
38 import paste.auth.basic
39 import paste.httpexceptions
39 import paste.httpexceptions
40 import paste.httpheaders
40 import paste.httpheaders
41 import webob.exc
41 import webob.exc
42 from tg import TGController, config, render_template, request, response, session
42 from tg import TGController, config, render_template, request, response, session
43 from tg import tmpl_context as c
43 from tg import tmpl_context as c
44 from tg.i18n import ugettext as _
44 from tg.i18n import ugettext as _
45
45
46 import kallithea
46 import kallithea
47 from kallithea.lib import auth_modules, ext_json, webutils
47 from kallithea.lib import auth_modules, ext_json, webutils
48 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
48 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
49 from kallithea.lib.exceptions import UserCreationError
49 from kallithea.lib.exceptions import UserCreationError
50 from kallithea.lib.utils import get_repo_slug, is_valid_repo
50 from kallithea.lib.utils import get_repo_slug, is_valid_repo
51 from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
51 from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
52 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
52 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
53 from kallithea.lib.webutils import url
53 from kallithea.lib.webutils import url
54 from kallithea.model import db, meta
54 from kallithea.model import db, meta
55 from kallithea.model.scm import ScmModel
55 from kallithea.model.scm import ScmModel
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def render(template_path):
61 def render(template_path):
62 return render_template({'url': url}, 'mako', template_path)
62 return render_template({'url': url}, 'mako', template_path)
63
63
64
64
65 def _filter_proxy(ip):
65 def _filter_proxy(ip):
66 """
66 """
67 HTTP_X_FORWARDED_FOR headers can have multiple IP addresses, with the
67 HTTP_X_FORWARDED_FOR headers can have multiple IP addresses, with the
68 leftmost being the original client. Each proxy that is forwarding the
68 leftmost being the original client. Each proxy that is forwarding the
69 request will usually add the IP address it sees the request coming from.
69 request will usually add the IP address it sees the request coming from.
70
70
71 The client might have provided a fake leftmost value before hitting the
71 The client might have provided a fake leftmost value before hitting the
72 first proxy, so if we have a proxy that is adding one IP address, we can
72 first proxy, so if we have a proxy that is adding one IP address, we can
73 only trust the rightmost address.
73 only trust the rightmost address.
74 """
74 """
75 if ',' in ip:
75 if ',' in ip:
76 _ips = ip.split(',')
76 _ips = ip.split(',')
77 _first_ip = _ips[-1].strip()
77 _first_ip = _ips[-1].strip()
78 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
78 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
79 return _first_ip
79 return _first_ip
80 return ip
80 return ip
81
81
82
82
83 def get_ip_addr(environ):
83 def get_ip_addr(environ):
84 """The web server will set REMOTE_ADDR to the unfakeable IP layer client IP address.
84 """The web server will set REMOTE_ADDR to the unfakeable IP layer client IP address.
85 If using a proxy server, make it possible to use another value, such as
85 If using a proxy server, make it possible to use another value, such as
86 the X-Forwarded-For header, by setting `remote_addr_variable = HTTP_X_FORWARDED_FOR`.
86 the X-Forwarded-For header, by setting `remote_addr_variable = HTTP_X_FORWARDED_FOR`.
87 """
87 """
88 remote_addr_variable = kallithea.CONFIG.get('remote_addr_variable', 'REMOTE_ADDR')
88 remote_addr_variable = kallithea.CONFIG.get('remote_addr_variable', 'REMOTE_ADDR')
89 return _filter_proxy(environ.get(remote_addr_variable, '0.0.0.0'))
89 return _filter_proxy(environ.get(remote_addr_variable, '0.0.0.0'))
90
90
91
91
92 def get_path_info(environ):
92 def get_path_info(environ):
93 """Return PATH_INFO from environ ... using tg.original_request if available.
93 """Return PATH_INFO from environ ... using tg.original_request if available.
94
94
95 In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
95 In Python 3 WSGI, PATH_INFO is a unicode str, but kind of contains encoded
96 bytes. The code points are guaranteed to only use the lower 8 bit bits, and
96 bytes. The code points are guaranteed to only use the lower 8 bit bits, and
97 encoding the string with the 1:1 encoding latin1 will give the
97 encoding the string with the 1:1 encoding latin1 will give the
98 corresponding byte string ... which then can be decoded to proper unicode.
98 corresponding byte string ... which then can be decoded to proper unicode.
99 """
99 """
100 org_req = environ.get('tg.original_request')
100 org_req = environ.get('tg.original_request')
101 if org_req is not None:
101 if org_req is not None:
102 environ = org_req.environ
102 environ = org_req.environ
103 return safe_str(environ['PATH_INFO'].encode('latin1'))
103 return safe_str(environ['PATH_INFO'].encode('latin1'))
104
104
105
105
106 def log_in_user(user, remember, is_external_auth, ip_addr):
106 def log_in_user(user, remember, is_external_auth, ip_addr):
107 """
107 """
108 Log a `User` in and update session and cookies. If `remember` is True,
108 Log a `User` in and update session and cookies. If `remember` is True,
109 the session cookie is set to expire in a year; otherwise, it expires at
109 the session cookie is set to expire in a year; otherwise, it expires at
110 the end of the browser session.
110 the end of the browser session.
111
111
112 Returns populated `AuthUser` object.
112 Returns populated `AuthUser` object.
113 """
113 """
114 # It should not be possible to explicitly log in as the default user.
114 # It should not be possible to explicitly log in as the default user.
115 assert not user.is_default_user, user
115 assert not user.is_default_user, user
116
116
117 auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
117 auth_user = AuthUser.make(dbuser=user, is_external_auth=is_external_auth, ip_addr=ip_addr)
118 if auth_user is None:
118 if auth_user is None:
119 return None
119 return None
120
120
121 user.update_lastlogin()
121 user.update_lastlogin()
122 meta.Session().commit()
122 meta.Session().commit()
123
123
124 # Start new session to prevent session fixation attacks.
124 # Start new session to prevent session fixation attacks.
125 session.invalidate()
125 session.invalidate()
126 session['authuser'] = cookie = auth_user.to_cookie()
126 session['authuser'] = cookie = auth_user.to_cookie()
127
127
128 # If they want to be remembered, update the cookie.
128 # If they want to be remembered, update the cookie.
129 # NOTE: Assumes that beaker defaults to browser session cookie.
129 # NOTE: Assumes that beaker defaults to browser session cookie.
130 if remember:
130 if remember:
131 t = datetime.datetime.now() + datetime.timedelta(days=365)
131 t = datetime.datetime.now() + datetime.timedelta(days=365)
132 session._set_cookie_expires(t)
132 session._set_cookie_expires(t)
133
133
134 session.save()
134 session.save()
135
135
136 log.info('user %s is now authenticated and stored in '
136 log.info('user %s is now authenticated and stored in '
137 'session, session attrs %s', user.username, cookie)
137 'session, session attrs %s', user.username, cookie)
138
138
139 # dumps session attrs back to cookie
139 # dumps session attrs back to cookie
140 session._update_cookie_out()
140 session._update_cookie_out()
141
141
142 return auth_user
142 return auth_user
143
143
144
144
145 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
145 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
146
146
147 def __init__(self, realm, authfunc, auth_http_code=None):
147 def __init__(self, realm, authfunc, auth_http_code=None):
148 self.realm = realm
148 self.realm = realm
149 self.authfunc = authfunc
149 self.authfunc = authfunc
150 self._rc_auth_http_code = auth_http_code
150 self._rc_auth_http_code = auth_http_code
151
151
152 def build_authentication(self, environ):
152 def build_authentication(self, environ):
153 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
153 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
154 # Consume the whole body before sending a response
154 # Consume the whole body before sending a response
155 try:
155 try:
156 request_body_size = int(environ.get('CONTENT_LENGTH', 0))
156 request_body_size = int(environ.get('CONTENT_LENGTH', 0))
157 except (ValueError):
157 except (ValueError):
158 request_body_size = 0
158 request_body_size = 0
159 environ['wsgi.input'].read(request_body_size)
159 environ['wsgi.input'].read(request_body_size)
160 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
160 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
161 # return 403 if alternative http return code is specified in
161 # return 403 if alternative http return code is specified in
162 # Kallithea config
162 # Kallithea config
163 return paste.httpexceptions.HTTPForbidden(headers=head)
163 return paste.httpexceptions.HTTPForbidden(headers=head)
164 return paste.httpexceptions.HTTPUnauthorized(headers=head)
164 return paste.httpexceptions.HTTPUnauthorized(headers=head)
165
165
166 def authenticate(self, environ):
166 def authenticate(self, environ):
167 authorization = paste.httpheaders.AUTHORIZATION(environ)
167 authorization = paste.httpheaders.AUTHORIZATION(environ)
168 if not authorization:
168 if not authorization:
169 return self.build_authentication(environ)
169 return self.build_authentication(environ)
170 (authmeth, auth) = authorization.split(' ', 1)
170 (authmeth, auth) = authorization.split(' ', 1)
171 if 'basic' != authmeth.lower():
171 if 'basic' != authmeth.lower():
172 return self.build_authentication(environ)
172 return self.build_authentication(environ)
173 auth = safe_str(base64.b64decode(auth.strip()))
173 auth = safe_str(base64.b64decode(auth.strip()))
174 _parts = auth.split(':', 1)
174 _parts = auth.split(':', 1)
175 if len(_parts) == 2:
175 if len(_parts) == 2:
176 username, password = _parts
176 username, password = _parts
177 if self.authfunc(username, password, environ) is not None:
177 if self.authfunc(username, password, environ) is not None:
178 return username
178 return username
179 return self.build_authentication(environ)
179 return self.build_authentication(environ)
180
180
181 __call__ = authenticate
181 __call__ = authenticate
182
182
183
183
184 class BaseVCSController(object):
184 class BaseVCSController(object):
185 """Base controller for handling Mercurial/Git protocol requests
185 """Base controller for handling Mercurial/Git protocol requests
186 (coming from a VCS client, and not a browser).
186 (coming from a VCS client, and not a browser).
187 """
187 """
188
188
189 scm_alias = None # 'hg' / 'git'
189 scm_alias = None # 'hg' / 'git'
190
190
191 def __init__(self, application, config):
191 def __init__(self, application, config):
192 self.application = application
192 self.application = application
193 self.config = config
193 self.config = config
194 # base path of repo locations
194 # base path of repo locations
195 self.basepath = self.config['base_path']
195 self.basepath = self.config['base_path']
196 # authenticate this VCS request using the authentication modules
196 # authenticate this VCS request using the authentication modules
197 self.authenticate = BasicAuth('', auth_modules.authenticate,
197 self.authenticate = BasicAuth('', auth_modules.authenticate,
198 config.get('auth_ret_code'))
198 config.get('auth_ret_code'))
199
199
200 @classmethod
200 @classmethod
201 def parse_request(cls, environ):
201 def parse_request(cls, environ):
202 """If request is parsed as a request for this VCS, return a namespace with the parsed request.
202 """If request is parsed as a request for this VCS, return a namespace with the parsed request.
203 If the request is unknown, return None.
203 If the request is unknown, return None.
204 """
204 """
205 raise NotImplementedError()
205 raise NotImplementedError()
206
206
207 def _authorize(self, environ, action, repo_name, ip_addr):
207 def _authorize(self, environ, action, repo_name, ip_addr):
208 """Authenticate and authorize user.
208 """Authenticate and authorize user.
209
209
210 Since we're dealing with a VCS client and not a browser, we only
210 Since we're dealing with a VCS client and not a browser, we only
211 support HTTP basic authentication, either directly via raw header
211 support HTTP basic authentication, either directly via raw header
212 inspection, or by using container authentication to delegate the
212 inspection, or by using container authentication to delegate the
213 authentication to the web server.
213 authentication to the web server.
214
214
215 Returns (user, None) on successful authentication and authorization.
215 Returns (user, None) on successful authentication and authorization.
216 Returns (None, wsgi_app) to send the wsgi_app response to the client.
216 Returns (None, wsgi_app) to send the wsgi_app response to the client.
217 """
217 """
218 # Use anonymous access if allowed for action on repo.
218 # Use anonymous access if allowed for action on repo.
219 default_user = db.User.get_default_user()
219 default_user = db.User.get_default_user()
220 default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
220 default_authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
221 if default_authuser is None:
221 if default_authuser is None:
222 log.debug('No anonymous access at all') # move on to proper user auth
222 log.debug('No anonymous access at all') # move on to proper user auth
223 else:
223 else:
224 if self._check_permission(action, default_authuser, repo_name):
224 if self._check_permission(action, default_authuser, repo_name):
225 return default_authuser, None
225 return default_authuser, None
226 log.debug('Not authorized to access this repository as anonymous user')
226 log.debug('Not authorized to access this repository as anonymous user')
227
227
228 username = None
228 username = None
229 #==============================================================
229 #==============================================================
230 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
230 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
231 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
231 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
232 #==============================================================
232 #==============================================================
233
233
234 # try to auth based on environ, container auth methods
234 # try to auth based on environ, container auth methods
235 log.debug('Running PRE-AUTH for container based authentication')
235 log.debug('Running PRE-AUTH for container based authentication')
236 pre_auth = auth_modules.authenticate('', '', environ)
236 pre_auth = auth_modules.authenticate('', '', environ)
237 if pre_auth is not None and pre_auth.get('username'):
237 if pre_auth is not None and pre_auth.get('username'):
238 username = pre_auth['username']
238 username = pre_auth['username']
239 log.debug('PRE-AUTH got %s as username', username)
239 log.debug('PRE-AUTH got %s as username', username)
240
240
241 # If not authenticated by the container, running basic auth
241 # If not authenticated by the container, running basic auth
242 if not username:
242 if not username:
243 self.authenticate.realm = self.config['realm']
243 self.authenticate.realm = self.config['realm']
244 result = self.authenticate(environ)
244 result = self.authenticate(environ)
245 if isinstance(result, str):
245 if isinstance(result, str):
246 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
246 paste.httpheaders.AUTH_TYPE.update(environ, 'basic')
247 paste.httpheaders.REMOTE_USER.update(environ, result)
247 paste.httpheaders.REMOTE_USER.update(environ, result)
248 username = result
248 username = result
249 else:
249 else:
250 return None, result.wsgi_application
250 return None, result.wsgi_application
251
251
252 #==============================================================
252 #==============================================================
253 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
253 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
254 #==============================================================
254 #==============================================================
255 try:
255 try:
256 user = db.User.get_by_username_or_email(username)
256 user = db.User.get_by_username_or_email(username)
257 except Exception:
257 except Exception:
258 log.error(traceback.format_exc())
258 log.error(traceback.format_exc())
259 return None, webob.exc.HTTPInternalServerError()
259 return None, webob.exc.HTTPInternalServerError()
260
260
261 authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
261 authuser = AuthUser.make(dbuser=user, ip_addr=ip_addr)
262 if authuser is None:
262 if authuser is None:
263 return None, webob.exc.HTTPForbidden()
263 return None, webob.exc.HTTPForbidden()
264 if not self._check_permission(action, authuser, repo_name):
264 if not self._check_permission(action, authuser, repo_name):
265 return None, webob.exc.HTTPForbidden()
265 return None, webob.exc.HTTPForbidden()
266
266
267 return user, None
267 return user, None
268
268
269 def _handle_request(self, environ, start_response):
269 def _handle_request(self, environ, start_response):
270 raise NotImplementedError()
270 raise NotImplementedError()
271
271
272 def _check_permission(self, action, authuser, repo_name):
272 def _check_permission(self, action, authuser, repo_name):
273 """
273 """
274 :param action: 'push' or 'pull'
274 :param action: 'push' or 'pull'
275 :param user: `AuthUser` instance
275 :param user: `AuthUser` instance
276 :param repo_name: repository name
276 :param repo_name: repository name
277 """
277 """
278 if action == 'push':
278 if action == 'push':
279 if not HasPermissionAnyMiddleware('repository.write',
279 if not HasPermissionAnyMiddleware('repository.write',
280 'repository.admin')(authuser,
280 'repository.admin')(authuser,
281 repo_name):
281 repo_name):
282 return False
282 return False
283
283
284 elif action == 'pull':
284 elif action == 'pull':
285 #any other action need at least read permission
285 #any other action need at least read permission
286 if not HasPermissionAnyMiddleware('repository.read',
286 if not HasPermissionAnyMiddleware('repository.read',
287 'repository.write',
287 'repository.write',
288 'repository.admin')(authuser,
288 'repository.admin')(authuser,
289 repo_name):
289 repo_name):
290 return False
290 return False
291
291
292 else:
292 else:
293 assert False, action
293 assert False, action
294
294
295 return True
295 return True
296
296
297 def __call__(self, environ, start_response):
297 def __call__(self, environ, start_response):
298 try:
298 try:
299 # try parsing a request for this VCS - if it fails, call the wrapped app
299 # try parsing a request for this VCS - if it fails, call the wrapped app
300 parsed_request = self.parse_request(environ)
300 parsed_request = self.parse_request(environ)
301 if parsed_request is None:
301 if parsed_request is None:
302 return self.application(environ, start_response)
302 return self.application(environ, start_response)
303
303
304 # skip passing error to error controller
304 # skip passing error to error controller
305 environ['pylons.status_code_redirect'] = True
305 environ['pylons.status_code_redirect'] = True
306
306
307 # quick check if repo exists...
307 # quick check if repo exists...
308 if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
308 if not is_valid_repo(parsed_request.repo_name, self.basepath, self.scm_alias):
309 raise webob.exc.HTTPNotFound()
309 raise webob.exc.HTTPNotFound()
310
310
311 if parsed_request.action is None:
311 if parsed_request.action is None:
312 # Note: the client doesn't get the helpful error message
312 # Note: the client doesn't get the helpful error message
313 raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
313 raise webob.exc.HTTPBadRequest('Unable to detect pull/push action for %r! Are you using a nonstandard command or client?' % parsed_request.repo_name)
314
314
315 #======================================================================
315 #======================================================================
316 # CHECK PERMISSIONS
316 # CHECK PERMISSIONS
317 #======================================================================
317 #======================================================================
318 ip_addr = get_ip_addr(environ)
318 ip_addr = get_ip_addr(environ)
319 user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
319 user, response_app = self._authorize(environ, parsed_request.action, parsed_request.repo_name, ip_addr)
320 if response_app is not None:
320 if response_app is not None:
321 return response_app(environ, start_response)
321 return response_app(environ, start_response)
322
322
323 #======================================================================
323 #======================================================================
324 # REQUEST HANDLING
324 # REQUEST HANDLING
325 #======================================================================
325 #======================================================================
326 set_hook_environment(user.username, ip_addr,
326 set_hook_environment(user.username, ip_addr,
327 parsed_request.repo_name, self.scm_alias, parsed_request.action)
327 parsed_request.repo_name, self.scm_alias, parsed_request.action)
328
328
329 try:
329 try:
330 log.info('%s action on %s repo "%s" by "%s" from %s',
330 log.info('%s action on %s repo "%s" by "%s" from %s',
331 parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
331 parsed_request.action, self.scm_alias, parsed_request.repo_name, user.username, ip_addr)
332 app = self._make_app(parsed_request)
332 app = self._make_app(parsed_request)
333 return app(environ, start_response)
333 return app(environ, start_response)
334 except Exception:
334 except Exception:
335 log.error(traceback.format_exc())
335 log.error(traceback.format_exc())
336 raise webob.exc.HTTPInternalServerError()
336 raise webob.exc.HTTPInternalServerError()
337
337
338 except webob.exc.HTTPException as e:
338 except webob.exc.HTTPException as e:
339 return e(environ, start_response)
339 return e(environ, start_response)
340
340
341
341
342 class BaseController(TGController):
342 class BaseController(TGController):
343
343
344 def _before(self, *args, **kwargs):
344 def _before(self, *args, **kwargs):
345 """
345 """
346 _before is called before controller methods and after __call__
346 _before is called before controller methods and after __call__
347 """
347 """
348 if request.needs_csrf_check:
348 if request.needs_csrf_check:
349 # CSRF protection: Whenever a request has ambient authority (whether
349 # CSRF protection: Whenever a request has ambient authority (whether
350 # through a session cookie or its origin IP address), it must include
350 # through a session cookie or its origin IP address), it must include
351 # the correct token, unless the HTTP method is GET or HEAD (and thus
351 # the correct token, unless the HTTP method is GET or HEAD (and thus
352 # guaranteed to be side effect free. In practice, the only situation
352 # guaranteed to be side effect free. In practice, the only situation
353 # where we allow side effects without ambient authority is when the
353 # where we allow side effects without ambient authority is when the
354 # authority comes from an API key; and that is handled above.
354 # authority comes from an API key; and that is handled above.
355 token = request.POST.get(webutils.session_csrf_secret_name)
355 token = request.POST.get(webutils.session_csrf_secret_name)
356 if not token or token != webutils.session_csrf_secret_token():
356 if not token or token != webutils.session_csrf_secret_token():
357 log.error('CSRF check failed')
357 log.error('CSRF check failed')
358 raise webob.exc.HTTPForbidden()
358 raise webob.exc.HTTPForbidden()
359
359
360 c.kallithea_version = kallithea.__version__
360 c.kallithea_version = kallithea.__version__
361 settings = db.Setting.get_app_settings()
361 settings = db.Setting.get_app_settings()
362
362
363 # Visual options
363 # Visual options
364 c.visual = AttributeDict({})
364 c.visual = AttributeDict({})
365
365
366 ## DB stored
366 ## DB stored
367 c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
367 c.visual.show_public_icon = asbool(settings.get('show_public_icon'))
368 c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
368 c.visual.show_private_icon = asbool(settings.get('show_private_icon'))
369 c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
369 c.visual.stylify_metalabels = asbool(settings.get('stylify_metalabels'))
370 c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
370 c.visual.page_size = safe_int(settings.get('dashboard_items', 100))
371 c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
371 c.visual.admin_grid_items = safe_int(settings.get('admin_grid_items', 100))
372 c.visual.repository_fields = asbool(settings.get('repository_fields'))
372 c.visual.repository_fields = asbool(settings.get('repository_fields'))
373 c.visual.show_version = asbool(settings.get('show_version'))
373 c.visual.show_version = asbool(settings.get('show_version'))
374 c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
374 c.visual.use_gravatar = asbool(settings.get('use_gravatar'))
375 c.visual.gravatar_url = settings.get('gravatar_url')
375 c.visual.gravatar_url = settings.get('gravatar_url')
376
376
377 c.ga_code = settings.get('ga_code')
377 c.ga_code = settings.get('ga_code')
378 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
378 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
379 if c.ga_code and '<' not in c.ga_code:
379 if c.ga_code and '<' not in c.ga_code:
380 c.ga_code = '''<script type="text/javascript">
380 c.ga_code = '''<script type="text/javascript">
381 var _gaq = _gaq || [];
381 var _gaq = _gaq || [];
382 _gaq.push(['_setAccount', '%s']);
382 _gaq.push(['_setAccount', '%s']);
383 _gaq.push(['_trackPageview']);
383 _gaq.push(['_trackPageview']);
384
384
385 (function() {
385 (function() {
386 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
386 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
387 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
387 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
388 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
388 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
389 })();
389 })();
390 </script>''' % c.ga_code
390 </script>''' % c.ga_code
391 c.site_name = settings.get('title')
391 c.site_name = settings.get('title')
392 c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
392 c.clone_uri_tmpl = settings.get('clone_uri_tmpl') or db.Repository.DEFAULT_CLONE_URI
393 c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH
393 c.clone_ssh_tmpl = settings.get('clone_ssh_tmpl') or db.Repository.DEFAULT_CLONE_SSH
394
394
395 ## INI stored
395 ## INI stored
396 c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
396 c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
397 c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
397 c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
398 c.ssh_enabled = asbool(config.get('ssh_enabled', False))
398 c.ssh_enabled = asbool(config.get('ssh_enabled', False))
399
399
400 c.instance_id = config.get('instance_id')
400 c.instance_id = config.get('instance_id')
401 c.issues_url = config.get('bugtracker', url('issues_url'))
401 c.issues_url = config.get('bugtracker', url('issues_url'))
402 # END CONFIG VARS
402 # END CONFIG VARS
403
403
404 c.repo_name = get_repo_slug(request) # can be empty
404 c.repo_name = get_repo_slug(request) # can be empty
405 c.backends = list(kallithea.BACKENDS)
405 c.backends = list(kallithea.BACKENDS)
406
406
407 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
407 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
408
408
409 c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
409 c.my_pr_count = db.PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count()
410
410
411 self.scm_model = ScmModel()
411 self.scm_model = ScmModel()
412
412
413 @staticmethod
413 @staticmethod
414 def _determine_auth_user(session_authuser, ip_addr):
414 def _determine_auth_user(session_authuser, ip_addr):
415 """
415 """
416 Create an `AuthUser` object given the API key/bearer token
416 Create an `AuthUser` object given the API key/bearer token
417 (if any) and the value of the authuser session cookie.
417 (if any) and the value of the authuser session cookie.
418 Returns None if no valid user is found (like not active or no access for IP).
418 Returns None if no valid user is found (like not active or no access for IP).
419 """
419 """
420
420
421 # Authenticate by session cookie
421 # Authenticate by session cookie
422 # In ancient login sessions, 'authuser' may not be a dict.
422 # In ancient login sessions, 'authuser' may not be a dict.
423 # In that case, the user will have to log in again.
423 # In that case, the user will have to log in again.
424 # v0.3 and earlier included an 'is_authenticated' key; if present,
424 # v0.3 and earlier included an 'is_authenticated' key; if present,
425 # this must be True.
425 # this must be True.
426 if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
426 if isinstance(session_authuser, dict) and session_authuser.get('is_authenticated', True):
427 return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
427 return AuthUser.from_cookie(session_authuser, ip_addr=ip_addr)
428
428
429 # Authenticate by auth_container plugin (if enabled)
429 # Authenticate by auth_container plugin (if enabled)
430 if any(
430 if any(
431 plugin.is_container_auth
431 plugin.is_container_auth
432 for plugin in auth_modules.get_auth_plugins()
432 for plugin in auth_modules.get_auth_plugins()
433 ):
433 ):
434 try:
434 try:
435 user_info = auth_modules.authenticate('', '', request.environ)
435 user_info = auth_modules.authenticate('', '', request.environ)
436 except UserCreationError as e:
436 except UserCreationError as e:
437 webutils.flash(e, 'error', logf=log.error)
437 webutils.flash(e, 'error', logf=log.error)
438 else:
438 else:
439 if user_info is not None:
439 if user_info is not None:
440 username = user_info['username']
440 username = user_info['username']
441 user = db.User.get_by_username(username, case_insensitive=True)
441 user = db.User.get_by_username(username, case_insensitive=True)
442 return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
442 return log_in_user(user, remember=False, is_external_auth=True, ip_addr=ip_addr)
443
443
444 # User is default user (if active) or anonymous
444 # User is default user (if active) or anonymous
445 default_user = db.User.get_default_user()
445 default_user = db.User.get_default_user()
446 authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
446 authuser = AuthUser.make(dbuser=default_user, ip_addr=ip_addr)
447 if authuser is None: # fall back to anonymous
447 if authuser is None: # fall back to anonymous
448 authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
448 authuser = AuthUser(dbuser=default_user) # TODO: somehow use .make?
449 return authuser
449 return authuser
450
450
451 @staticmethod
451 @staticmethod
452 def _basic_security_checks():
452 def _basic_security_checks():
453 """Perform basic security/sanity checks before processing the request."""
453 """Perform basic security/sanity checks before processing the request."""
454
454
455 # Only allow the following HTTP request methods.
455 # Only allow the following HTTP request methods.
456 if request.method not in ['GET', 'HEAD', 'POST']:
456 if request.method not in ['GET', 'HEAD', 'POST']:
457 raise webob.exc.HTTPMethodNotAllowed()
457 raise webob.exc.HTTPMethodNotAllowed()
458
458
459 try:
460 params = request.params
461 except UnicodeDecodeError as e:
462 # webobj will leak UnicodeDecodeError when decoding invalid
463 # URLencoded byte sequences in parameters
464 log.error('Error decoding request parameters: %s' % e)
465 raise webob.exc.HTTPBadRequest()
466
459 # Also verify the _method override - no longer allowed.
467 # Also verify the _method override - no longer allowed.
460 if request.params.get('_method') is None:
468 if params.get('_method') is None:
461 pass # no override, no problem
469 pass # no override, no problem
462 else:
470 else:
463 raise webob.exc.HTTPMethodNotAllowed()
471 raise webob.exc.HTTPMethodNotAllowed()
464
472
465 # Make sure CSRF token never appears in the URL. If so, invalidate it.
473 # Make sure CSRF token never appears in the URL. If so, invalidate it.
466 if webutils.session_csrf_secret_name in request.GET:
474 if webutils.session_csrf_secret_name in request.GET:
467 log.error('CSRF key leak detected')
475 log.error('CSRF key leak detected')
468 session.pop(webutils.session_csrf_secret_name, None)
476 session.pop(webutils.session_csrf_secret_name, None)
469 session.save()
477 session.save()
470 webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
478 webutils.flash(_('CSRF token leak has been detected - all form tokens have been expired'),
471 category='error')
479 category='error')
472
480
473 # WebOb already ignores request payload parameters for anything other
481 # WebOb already ignores request payload parameters for anything other
474 # than POST/PUT, but double-check since other Kallithea code relies on
482 # than POST/PUT, but double-check since other Kallithea code relies on
475 # this assumption.
483 # this assumption.
476 if request.method not in ['POST', 'PUT'] and request.POST:
484 if request.method not in ['POST', 'PUT'] and request.POST:
477 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
485 log.error('%r request with payload parameters; WebOb should have stopped this', request.method)
478 raise webob.exc.HTTPBadRequest()
486 raise webob.exc.HTTPBadRequest()
479
487
480 def __call__(self, environ, context):
488 def __call__(self, environ, context):
481 try:
489 try:
482 ip_addr = get_ip_addr(environ)
490 ip_addr = get_ip_addr(environ)
483 self._basic_security_checks()
491 self._basic_security_checks()
484
492
485 api_key = request.GET.get('api_key')
493 api_key = request.GET.get('api_key')
486 try:
494 try:
487 # Request.authorization may raise ValueError on invalid input
495 # Request.authorization may raise ValueError on invalid input
488 type, params = request.authorization
496 type, params = request.authorization
489 except (ValueError, TypeError):
497 except (ValueError, TypeError):
490 pass
498 pass
491 else:
499 else:
492 if type.lower() == 'bearer':
500 if type.lower() == 'bearer':
493 api_key = params # bearer token is an api key too
501 api_key = params # bearer token is an api key too
494
502
495 if api_key is None:
503 if api_key is None:
496 authuser = self._determine_auth_user(
504 authuser = self._determine_auth_user(
497 session.get('authuser'),
505 session.get('authuser'),
498 ip_addr=ip_addr,
506 ip_addr=ip_addr,
499 )
507 )
500 needs_csrf_check = request.method not in ['GET', 'HEAD']
508 needs_csrf_check = request.method not in ['GET', 'HEAD']
501
509
502 else:
510 else:
503 dbuser = db.User.get_by_api_key(api_key)
511 dbuser = db.User.get_by_api_key(api_key)
504 if dbuser is None:
512 if dbuser is None:
505 log.info('No db user found for authentication with API key ****%s from %s',
513 log.info('No db user found for authentication with API key ****%s from %s',
506 api_key[-4:], ip_addr)
514 api_key[-4:], ip_addr)
507 authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
515 authuser = AuthUser.make(dbuser=dbuser, is_external_auth=True, ip_addr=ip_addr)
508 needs_csrf_check = False # API key provides CSRF protection
516 needs_csrf_check = False # API key provides CSRF protection
509
517
510 if authuser is None:
518 if authuser is None:
511 log.info('No valid user found')
519 log.info('No valid user found')
512 raise webob.exc.HTTPForbidden()
520 raise webob.exc.HTTPForbidden()
513
521
514 # set globals for auth user
522 # set globals for auth user
515 request.authuser = authuser
523 request.authuser = authuser
516 request.ip_addr = ip_addr
524 request.ip_addr = ip_addr
517 request.needs_csrf_check = needs_csrf_check
525 request.needs_csrf_check = needs_csrf_check
518
526
519 log.info('IP: %s User: %s Request: %s',
527 log.info('IP: %s User: %s Request: %s',
520 request.ip_addr, request.authuser,
528 request.ip_addr, request.authuser,
521 get_path_info(environ),
529 get_path_info(environ),
522 )
530 )
523 return super(BaseController, self).__call__(environ, context)
531 return super(BaseController, self).__call__(environ, context)
524 except webob.exc.HTTPException as e:
532 except webob.exc.HTTPException as e:
525 return e
533 return e
526
534
527
535
528 class BaseRepoController(BaseController):
536 class BaseRepoController(BaseController):
529 """
537 """
530 Base class for controllers responsible for loading all needed data for
538 Base class for controllers responsible for loading all needed data for
531 repository loaded items are
539 repository loaded items are
532
540
533 c.db_repo_scm_instance: instance of scm repository
541 c.db_repo_scm_instance: instance of scm repository
534 c.db_repo: instance of db
542 c.db_repo: instance of db
535 c.repository_followers: number of followers
543 c.repository_followers: number of followers
536 c.repository_forks: number of forks
544 c.repository_forks: number of forks
537 c.repository_following: weather the current user is following the current repo
545 c.repository_following: weather the current user is following the current repo
538 """
546 """
539
547
540 def _before(self, *args, **kwargs):
548 def _before(self, *args, **kwargs):
541 super(BaseRepoController, self)._before(*args, **kwargs)
549 super(BaseRepoController, self)._before(*args, **kwargs)
542 if c.repo_name: # extracted from request by base-base BaseController._before
550 if c.repo_name: # extracted from request by base-base BaseController._before
543 _dbr = db.Repository.get_by_repo_name(c.repo_name)
551 _dbr = db.Repository.get_by_repo_name(c.repo_name)
544 if not _dbr:
552 if not _dbr:
545 return
553 return
546
554
547 log.debug('Found repository in database %s with state `%s`',
555 log.debug('Found repository in database %s with state `%s`',
548 _dbr, _dbr.repo_state)
556 _dbr, _dbr.repo_state)
549 route = getattr(request.environ.get('routes.route'), 'name', '')
557 route = getattr(request.environ.get('routes.route'), 'name', '')
550
558
551 # allow to delete repos that are somehow damages in filesystem
559 # allow to delete repos that are somehow damages in filesystem
552 if route in ['delete_repo']:
560 if route in ['delete_repo']:
553 return
561 return
554
562
555 if _dbr.repo_state in [db.Repository.STATE_PENDING]:
563 if _dbr.repo_state in [db.Repository.STATE_PENDING]:
556 if route in ['repo_creating_home']:
564 if route in ['repo_creating_home']:
557 return
565 return
558 check_url = url('repo_creating_home', repo_name=c.repo_name)
566 check_url = url('repo_creating_home', repo_name=c.repo_name)
559 raise webob.exc.HTTPFound(location=check_url)
567 raise webob.exc.HTTPFound(location=check_url)
560
568
561 dbr = c.db_repo = _dbr
569 dbr = c.db_repo = _dbr
562 c.db_repo_scm_instance = c.db_repo.scm_instance
570 c.db_repo_scm_instance = c.db_repo.scm_instance
563 if c.db_repo_scm_instance is None:
571 if c.db_repo_scm_instance is None:
564 log.error('%s this repository is present in database but it '
572 log.error('%s this repository is present in database but it '
565 'cannot be created as an scm instance', c.repo_name)
573 'cannot be created as an scm instance', c.repo_name)
566 webutils.flash(_('Repository not found in the filesystem'),
574 webutils.flash(_('Repository not found in the filesystem'),
567 category='error')
575 category='error')
568 raise webob.exc.HTTPNotFound()
576 raise webob.exc.HTTPNotFound()
569
577
570 # some globals counter for menu
578 # some globals counter for menu
571 c.repository_followers = self.scm_model.get_followers(dbr)
579 c.repository_followers = self.scm_model.get_followers(dbr)
572 c.repository_forks = self.scm_model.get_forks(dbr)
580 c.repository_forks = self.scm_model.get_forks(dbr)
573 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
581 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
574 c.repository_following = self.scm_model.is_following_repo(
582 c.repository_following = self.scm_model.is_following_repo(
575 c.repo_name, request.authuser.user_id)
583 c.repo_name, request.authuser.user_id)
576
584
577 @staticmethod
585 @staticmethod
578 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
586 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
579 """
587 """
580 Safe way to get changeset. If error occurs show error.
588 Safe way to get changeset. If error occurs show error.
581 """
589 """
582 try:
590 try:
583 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
591 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
584 except EmptyRepositoryError as e:
592 except EmptyRepositoryError as e:
585 if returnempty:
593 if returnempty:
586 return repo.scm_instance.EMPTY_CHANGESET
594 return repo.scm_instance.EMPTY_CHANGESET
587 webutils.flash(_('There are no changesets yet'), category='error')
595 webutils.flash(_('There are no changesets yet'), category='error')
588 raise webob.exc.HTTPNotFound()
596 raise webob.exc.HTTPNotFound()
589 except ChangesetDoesNotExistError as e:
597 except ChangesetDoesNotExistError as e:
590 webutils.flash(_('Changeset for %s %s not found in %s') %
598 webutils.flash(_('Changeset for %s %s not found in %s') %
591 (ref_type, ref_name, repo.repo_name),
599 (ref_type, ref_name, repo.repo_name),
592 category='error')
600 category='error')
593 raise webob.exc.HTTPNotFound()
601 raise webob.exc.HTTPNotFound()
594 except RepositoryError as e:
602 except RepositoryError as e:
595 log.error(traceback.format_exc())
603 log.error(traceback.format_exc())
596 webutils.flash(e, category='error')
604 webutils.flash(e, category='error')
597 raise webob.exc.HTTPBadRequest()
605 raise webob.exc.HTTPBadRequest()
598
606
599
607
600 @decorator.decorator
608 @decorator.decorator
601 def jsonify(func, *args, **kwargs):
609 def jsonify(func, *args, **kwargs):
602 """Action decorator that formats output for JSON
610 """Action decorator that formats output for JSON
603
611
604 Given a function that will return content, this decorator will turn
612 Given a function that will return content, this decorator will turn
605 the result into JSON, with a content-type of 'application/json' and
613 the result into JSON, with a content-type of 'application/json' and
606 output it.
614 output it.
607 """
615 """
608 response.headers['Content-Type'] = 'application/json; charset=utf-8'
616 response.headers['Content-Type'] = 'application/json; charset=utf-8'
609 data = func(*args, **kwargs)
617 data = func(*args, **kwargs)
610 if isinstance(data, (list, tuple)):
618 if isinstance(data, (list, tuple)):
611 # A JSON list response is syntactically valid JavaScript and can be
619 # A JSON list response is syntactically valid JavaScript and can be
612 # loaded and executed as JavaScript by a malicious third-party site
620 # loaded and executed as JavaScript by a malicious third-party site
613 # using <script>, which can lead to cross-site data leaks.
621 # using <script>, which can lead to cross-site data leaks.
614 # JSON responses should therefore be scalars or objects (i.e. Python
622 # JSON responses should therefore be scalars or objects (i.e. Python
615 # dicts), because a JSON object is a syntax error if intepreted as JS.
623 # dicts), because a JSON object is a syntax error if intepreted as JS.
616 msg = "JSON responses with Array envelopes are susceptible to " \
624 msg = "JSON responses with Array envelopes are susceptible to " \
617 "cross-site data leak attacks, see " \
625 "cross-site data leak attacks, see " \
618 "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
626 "https://web.archive.org/web/20120519231904/http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
619 warnings.warn(msg, Warning, 2)
627 warnings.warn(msg, Warning, 2)
620 log.warning(msg)
628 log.warning(msg)
621 log.debug("Returning JSON wrapped action output")
629 log.debug("Returning JSON wrapped action output")
622 return ascii_bytes(ext_json.dumps(data))
630 return ascii_bytes(ext_json.dumps(data))
623
631
624 @decorator.decorator
632 @decorator.decorator
625 def IfSshEnabled(func, *args, **kwargs):
633 def IfSshEnabled(func, *args, **kwargs):
626 """Decorator for functions that can only be called if SSH access is enabled.
634 """Decorator for functions that can only be called if SSH access is enabled.
627
635
628 If SSH access is disabled in the configuration file, HTTPNotFound is raised.
636 If SSH access is disabled in the configuration file, HTTPNotFound is raised.
629 """
637 """
630 if not c.ssh_enabled:
638 if not c.ssh_enabled:
631 webutils.flash(_("SSH access is disabled."), category='warning')
639 webutils.flash(_("SSH access is disabled."), category='warning')
632 raise webob.exc.HTTPNotFound()
640 raise webob.exc.HTTPNotFound()
633 return func(*args, **kwargs)
641 return func(*args, **kwargs)
@@ -1,233 +1,234
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3 <%block name="title">
3 <%block name="title">
4 ${_('About')}
4 ${_('About')}
5 </%block>
5 </%block>
6 <%block name="header_menu">
6 <%block name="header_menu">
7 ${self.menu('about')}
7 ${self.menu('about')}
8 </%block>
8 </%block>
9 <%def name="main()">
9 <%def name="main()">
10
10
11 <div class="panel panel-primary">
11 <div class="panel panel-primary">
12 <div class="panel-heading">
12 <div class="panel-heading">
13 <h5 class="panel-title">${_('About')} Kallithea</h5>
13 <h5 class="panel-title">${_('About')} Kallithea</h5>
14 </div>
14 </div>
15
15
16 <div class="panel-body panel-about">
16 <div class="panel-body panel-about">
17 <p><a href="https://kallithea-scm.org/">Kallithea</a> is a project of the
17 <p><a href="https://kallithea-scm.org/">Kallithea</a> is a project of the
18 <a href="http://sfconservancy.org/">Software Freedom Conservancy, Inc.</a>
18 <a href="http://sfconservancy.org/">Software Freedom Conservancy, Inc.</a>
19 and is released under the terms of the
19 and is released under the terms of the
20 <a href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License,
20 <a href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License,
21 v 3.0 (GPLv3)</a>.</p>
21 v 3.0 (GPLv3)</a>.</p>
22
22
23 <p>Kallithea is copyrighted by various authors, including but not
23 <p>Kallithea is copyrighted by various authors, including but not
24 necessarily limited to the following:</p>
24 necessarily limited to the following:</p>
25 <ul>
25 <ul>
26
26
27 <li>Copyright &copy; 2012&ndash;2024, Mads Kiilerich</li>
27 <li>Copyright &copy; 2012&ndash;2024, Mads Kiilerich</li>
28 <li>Copyright &copy; 2024, Aristotelis Stageiritis</li>
28 <li>Copyright &copy; 2024, Aristotelis Stageiritis</li>
29 <li>Copyright &copy; 2024, Poesty Li</li>
29 <li>Copyright &copy; 2024, Poesty Li</li>
30 <li>Copyright &copy; 2024, Valentin Kleibel</li>
30 <li>Copyright &copy; 2019&ndash;2020, 2022&ndash;2023, Manuel Jacob</li>
31 <li>Copyright &copy; 2019&ndash;2020, 2022&ndash;2023, Manuel Jacob</li>
31 <li>Copyright &copy; 2023, Mathias De Mare</li>
32 <li>Copyright &copy; 2023, Mathias De Mare</li>
32 <li>Copyright &copy; 2023, qy117121</li>
33 <li>Copyright &copy; 2023, qy117121</li>
33 <li>Copyright &copy; 2015&ndash;2017, 2019&ndash;2022, Γ‰tienne Gilli</li>
34 <li>Copyright &copy; 2015&ndash;2017, 2019&ndash;2022, Γ‰tienne Gilli</li>
34 <li>Copyright &copy; 2016&ndash;2017, 2020, 2022, Asterios Dimitriou</li>
35 <li>Copyright &copy; 2016&ndash;2017, 2020, 2022, Asterios Dimitriou</li>
35 <li>Copyright &copy; 2022, Jaime MarquΓ­nez FerrΓ‘ndiz</li>
36 <li>Copyright &copy; 2022, Jaime MarquΓ­nez FerrΓ‘ndiz</li>
36 <li>Copyright &copy; 2022, Louis Bertrand</li>
37 <li>Copyright &copy; 2022, Louis Bertrand</li>
37 <li>Copyright &copy; 2022, toras9000</li>
38 <li>Copyright &copy; 2022, toras9000</li>
38 <li>Copyright &copy; 2022, yzqzss</li>
39 <li>Copyright &copy; 2022, yzqzss</li>
39 <li>Copyright &copy; 2022, МАН69К</li>
40 <li>Copyright &copy; 2022, МАН69К</li>
40 <li>Copyright &copy; 2014&ndash;2021, Thomas De Schampheleire</li>
41 <li>Copyright &copy; 2014&ndash;2021, Thomas De Schampheleire</li>
41 <li>Copyright &copy; 2018&ndash;2021, ssantos</li>
42 <li>Copyright &copy; 2018&ndash;2021, ssantos</li>
42 <li>Copyright &copy; 2019&ndash;2021, Private</li>
43 <li>Copyright &copy; 2019&ndash;2021, Private</li>
43 <li>Copyright &copy; 2020&ndash;2021, fresh</li>
44 <li>Copyright &copy; 2020&ndash;2021, fresh</li>
44 <li>Copyright &copy; 2020&ndash;2021, robertus</li>
45 <li>Copyright &copy; 2020&ndash;2021, robertus</li>
45 <li>Copyright &copy; 2021, Eugenia Russell</li>
46 <li>Copyright &copy; 2021, Eugenia Russell</li>
46 <li>Copyright &copy; 2021, Michalis</li>
47 <li>Copyright &copy; 2021, Michalis</li>
47 <li>Copyright &copy; 2021, vs</li>
48 <li>Copyright &copy; 2021, vs</li>
48 <li>Copyright &copy; 2021, АлСксандр</li>
49 <li>Copyright &copy; 2021, АлСксандр</li>
49 <li>Copyright &copy; 2017&ndash;2020, Allan NordhΓΈy</li>
50 <li>Copyright &copy; 2017&ndash;2020, Allan NordhΓΈy</li>
50 <li>Copyright &copy; 2017, 2020, Anton Schur</li>
51 <li>Copyright &copy; 2017, 2020, Anton Schur</li>
51 <li>Copyright &copy; 2020, Artem</li>
52 <li>Copyright &copy; 2020, Artem</li>
52 <li>Copyright &copy; 2020, David Ignjić</li>
53 <li>Copyright &copy; 2020, David Ignjić</li>
53 <li>Copyright &copy; 2020, Dennis Fink</li>
54 <li>Copyright &copy; 2020, Dennis Fink</li>
54 <li>Copyright &copy; 2020, J. Lavoie</li>
55 <li>Copyright &copy; 2020, J. Lavoie</li>
55 <li>Copyright &copy; 2020, Ross Thomas</li>
56 <li>Copyright &copy; 2020, Ross Thomas</li>
56 <li>Copyright &copy; 2020, Tim Ooms</li>
57 <li>Copyright &copy; 2020, Tim Ooms</li>
57 <li>Copyright &copy; 2012, 2014&ndash;2017, 2019, Andrej Shadura</li>
58 <li>Copyright &copy; 2012, 2014&ndash;2017, 2019, Andrej Shadura</li>
58 <li>Copyright &copy; 2019, Adi Kriegisch</li>
59 <li>Copyright &copy; 2019, Adi Kriegisch</li>
59 <li>Copyright &copy; 2019, Danni Randeris</li>
60 <li>Copyright &copy; 2019, Danni Randeris</li>
60 <li>Copyright &copy; 2019, Edmund Wong</li>
61 <li>Copyright &copy; 2019, Edmund Wong</li>
61 <li>Copyright &copy; 2019, Elizabeth Sherrock</li>
62 <li>Copyright &copy; 2019, Elizabeth Sherrock</li>
62 <li>Copyright &copy; 2019, Hüseyin Tunç</li>
63 <li>Copyright &copy; 2019, Hüseyin Tunç</li>
63 <li>Copyright &copy; 2019, leela</li>
64 <li>Copyright &copy; 2019, leela</li>
64 <li>Copyright &copy; 2019, Mateusz Mendel</li>
65 <li>Copyright &copy; 2019, Mateusz Mendel</li>
65 <li>Copyright &copy; 2019, Nathan</li>
66 <li>Copyright &copy; 2019, Nathan</li>
66 <li>Copyright &copy; 2019, Oleksandr Shtalinberg</li>
67 <li>Copyright &copy; 2019, Oleksandr Shtalinberg</li>
67 <li>Copyright &copy; 2019, THANOS SIOURDAKIS</li>
68 <li>Copyright &copy; 2019, THANOS SIOURDAKIS</li>
68 <li>Copyright &copy; 2019, Wolfgang Scherer</li>
69 <li>Copyright &copy; 2019, Wolfgang Scherer</li>
69 <li>Copyright &copy; 2019, Π₯ристо Π‘Ρ‚Π°Π½Π΅Π²</li>
70 <li>Copyright &copy; 2019, Π₯ристо Π‘Ρ‚Π°Π½Π΅Π²</li>
70 <li>Copyright &copy; 2012, 2014&ndash;2018, Dominik Ruf</li>
71 <li>Copyright &copy; 2012, 2014&ndash;2018, Dominik Ruf</li>
71 <li>Copyright &copy; 2014&ndash;2015, 2018, Michal ČihaΕ™</li>
72 <li>Copyright &copy; 2014&ndash;2015, 2018, Michal ČihaΕ™</li>
72 <li>Copyright &copy; 2015, 2018, Branko Majic</li>
73 <li>Copyright &copy; 2015, 2018, Branko Majic</li>
73 <li>Copyright &copy; 2018, Chris Rule</li>
74 <li>Copyright &copy; 2018, Chris Rule</li>
74 <li>Copyright &copy; 2018, JesΓΊs SΓ‘nchez</li>
75 <li>Copyright &copy; 2018, JesΓΊs SΓ‘nchez</li>
75 <li>Copyright &copy; 2018, Patrick Vane</li>
76 <li>Copyright &copy; 2018, Patrick Vane</li>
76 <li>Copyright &copy; 2018, Pheng Heong Tan</li>
77 <li>Copyright &copy; 2018, Pheng Heong Tan</li>
77 <li>Copyright &copy; 2018, Максим Π―ΠΊΠΈΠΌΡ‡ΡƒΠΊ</li>
78 <li>Copyright &copy; 2018, Максим Π―ΠΊΠΈΠΌΡ‡ΡƒΠΊ</li>
78 <li>Copyright &copy; 2018, ΠœΠ°Ρ€Ρ Π―ΠΌΠ±Π°Ρ€</li>
79 <li>Copyright &copy; 2018, ΠœΠ°Ρ€Ρ Π―ΠΌΠ±Π°Ρ€</li>
79 <li>Copyright &copy; 2012&ndash;2017, Unity Technologies</li>
80 <li>Copyright &copy; 2012&ndash;2017, Unity Technologies</li>
80 <li>Copyright &copy; 2015&ndash;2017, SΓΈren LΓΈvborg</li>
81 <li>Copyright &copy; 2015&ndash;2017, SΓΈren LΓΈvborg</li>
81 <li>Copyright &copy; 2015, 2017, Sam Jaques</li>
82 <li>Copyright &copy; 2015, 2017, Sam Jaques</li>
82 <li>Copyright &copy; 2017, Alessandro Molina</li>
83 <li>Copyright &copy; 2017, Alessandro Molina</li>
83 <li>Copyright &copy; 2017, Ching-Chen Mao</li>
84 <li>Copyright &copy; 2017, Ching-Chen Mao</li>
84 <li>Copyright &copy; 2017, Eivind Tagseth</li>
85 <li>Copyright &copy; 2017, Eivind Tagseth</li>
85 <li>Copyright &copy; 2017, FUJIWARA Katsunori</li>
86 <li>Copyright &copy; 2017, FUJIWARA Katsunori</li>
86 <li>Copyright &copy; 2017, Holger Schramm</li>
87 <li>Copyright &copy; 2017, Holger Schramm</li>
87 <li>Copyright &copy; 2017, Karl Goetz</li>
88 <li>Copyright &copy; 2017, Karl Goetz</li>
88 <li>Copyright &copy; 2017, Lars Kruse</li>
89 <li>Copyright &copy; 2017, Lars Kruse</li>
89 <li>Copyright &copy; 2017, Marko Semet</li>
90 <li>Copyright &copy; 2017, Marko Semet</li>
90 <li>Copyright &copy; 2017, Viktar Vauchkevich</li>
91 <li>Copyright &copy; 2017, Viktar Vauchkevich</li>
91 <li>Copyright &copy; 2012&ndash;2016, Takumi IINO</li>
92 <li>Copyright &copy; 2012&ndash;2016, Takumi IINO</li>
92 <li>Copyright &copy; 2015&ndash;2016, Jan Heylen</li>
93 <li>Copyright &copy; 2015&ndash;2016, Jan Heylen</li>
93 <li>Copyright &copy; 2015&ndash;2016, Robert Martinez</li>
94 <li>Copyright &copy; 2015&ndash;2016, Robert Martinez</li>
94 <li>Copyright &copy; 2015&ndash;2016, Robert Rauch</li>
95 <li>Copyright &copy; 2015&ndash;2016, Robert Rauch</li>
95 <li>Copyright &copy; 2016, Angel Ezquerra</li>
96 <li>Copyright &copy; 2016, Angel Ezquerra</li>
96 <li>Copyright &copy; 2016, Anton Shestakov</li>
97 <li>Copyright &copy; 2016, Anton Shestakov</li>
97 <li>Copyright &copy; 2016, Brandon Jones</li>
98 <li>Copyright &copy; 2016, Brandon Jones</li>
98 <li>Copyright &copy; 2016, Kateryna Musina</li>
99 <li>Copyright &copy; 2016, Kateryna Musina</li>
99 <li>Copyright &copy; 2016, Konstantin Veretennicov</li>
100 <li>Copyright &copy; 2016, Konstantin Veretennicov</li>
100 <li>Copyright &copy; 2016, Oscar Curero</li>
101 <li>Copyright &copy; 2016, Oscar Curero</li>
101 <li>Copyright &copy; 2016, Robert James Dennington</li>
102 <li>Copyright &copy; 2016, Robert James Dennington</li>
102 <li>Copyright &copy; 2016, timeless@gmail.com</li>
103 <li>Copyright &copy; 2016, timeless@gmail.com</li>
103 <li>Copyright &copy; 2016, YFdyh000</li>
104 <li>Copyright &copy; 2016, YFdyh000</li>
104 <li>Copyright &copy; 2012&ndash;2013, 2015, Aras Pranckevičius</li>
105 <li>Copyright &copy; 2012&ndash;2013, 2015, Aras Pranckevičius</li>
105 <li>Copyright &copy; 2014&ndash;2015, Bradley M. Kuhn</li>
106 <li>Copyright &copy; 2014&ndash;2015, Bradley M. Kuhn</li>
106 <li>Copyright &copy; 2014&ndash;2015, Christian Oyarzun</li>
107 <li>Copyright &copy; 2014&ndash;2015, Christian Oyarzun</li>
107 <li>Copyright &copy; 2014&ndash;2015, Joseph Rivera</li>
108 <li>Copyright &copy; 2014&ndash;2015, Joseph Rivera</li>
108 <li>Copyright &copy; 2014&ndash;2015, Sean Farley</li>
109 <li>Copyright &copy; 2014&ndash;2015, Sean Farley</li>
109 <li>Copyright &copy; 2015, Anatoly Bubenkov</li>
110 <li>Copyright &copy; 2015, Anatoly Bubenkov</li>
110 <li>Copyright &copy; 2015, Andrew Bartlett</li>
111 <li>Copyright &copy; 2015, Andrew Bartlett</li>
111 <li>Copyright &copy; 2015, BalÑzs Úr</li>
112 <li>Copyright &copy; 2015, BalÑzs Úr</li>
112 <li>Copyright &copy; 2015, Ben Finney</li>
113 <li>Copyright &copy; 2015, Ben Finney</li>
113 <li>Copyright &copy; 2015, Daniel Hobley</li>
114 <li>Copyright &copy; 2015, Daniel Hobley</li>
114 <li>Copyright &copy; 2015, David Avigni</li>
115 <li>Copyright &copy; 2015, David Avigni</li>
115 <li>Copyright &copy; 2015, Denis Blanchette</li>
116 <li>Copyright &copy; 2015, Denis Blanchette</li>
116 <li>Copyright &copy; 2015, duanhongyi</li>
117 <li>Copyright &copy; 2015, duanhongyi</li>
117 <li>Copyright &copy; 2015, EriCSN Chang</li>
118 <li>Copyright &copy; 2015, EriCSN Chang</li>
118 <li>Copyright &copy; 2015, Grzegorz Krason</li>
119 <li>Copyright &copy; 2015, Grzegorz Krason</li>
119 <li>Copyright &copy; 2015, JiΕ™Γ­ Suchan</li>
120 <li>Copyright &copy; 2015, JiΕ™Γ­ Suchan</li>
120 <li>Copyright &copy; 2015, Kazunari Kobayashi</li>
121 <li>Copyright &copy; 2015, Kazunari Kobayashi</li>
121 <li>Copyright &copy; 2015, Kevin Bullock</li>
122 <li>Copyright &copy; 2015, Kevin Bullock</li>
122 <li>Copyright &copy; 2015, kobanari</li>
123 <li>Copyright &copy; 2015, kobanari</li>
123 <li>Copyright &copy; 2015, Marc Abramowitz</li>
124 <li>Copyright &copy; 2015, Marc Abramowitz</li>
124 <li>Copyright &copy; 2015, Marc Villetard</li>
125 <li>Copyright &copy; 2015, Marc Villetard</li>
125 <li>Copyright &copy; 2015, Matthias Zilk</li>
126 <li>Copyright &copy; 2015, Matthias Zilk</li>
126 <li>Copyright &copy; 2015, Michael Pohl</li>
127 <li>Copyright &copy; 2015, Michael Pohl</li>
127 <li>Copyright &copy; 2015, Michael V. DePalatis</li>
128 <li>Copyright &copy; 2015, Michael V. DePalatis</li>
128 <li>Copyright &copy; 2015, Morten Skaaning</li>
129 <li>Copyright &copy; 2015, Morten Skaaning</li>
129 <li>Copyright &copy; 2015, Nick High</li>
130 <li>Copyright &copy; 2015, Nick High</li>
130 <li>Copyright &copy; 2015, Niemand Jedermann</li>
131 <li>Copyright &copy; 2015, Niemand Jedermann</li>
131 <li>Copyright &copy; 2015, Peter Vitt</li>
132 <li>Copyright &copy; 2015, Peter Vitt</li>
132 <li>Copyright &copy; 2015, Ronny Pfannschmidt</li>
133 <li>Copyright &copy; 2015, Ronny Pfannschmidt</li>
133 <li>Copyright &copy; 2015, Tuux</li>
134 <li>Copyright &copy; 2015, Tuux</li>
134 <li>Copyright &copy; 2015, Viktar Palstsiuk</li>
135 <li>Copyright &copy; 2015, Viktar Palstsiuk</li>
135 <li>Copyright &copy; 2014, Ante Ilic</li>
136 <li>Copyright &copy; 2014, Ante Ilic</li>
136 <li>Copyright &copy; 2014, Calinou</li>
137 <li>Copyright &copy; 2014, Calinou</li>
137 <li>Copyright &copy; 2014, Daniel Anderson</li>
138 <li>Copyright &copy; 2014, Daniel Anderson</li>
138 <li>Copyright &copy; 2014, Henrik Stuart</li>
139 <li>Copyright &copy; 2014, Henrik Stuart</li>
139 <li>Copyright &copy; 2014, Ingo von Borstel</li>
140 <li>Copyright &copy; 2014, Ingo von Borstel</li>
140 <li>Copyright &copy; 2014, invision70</li>
141 <li>Copyright &copy; 2014, invision70</li>
141 <li>Copyright &copy; 2014, Jelmer VernooΔ³</li>
142 <li>Copyright &copy; 2014, Jelmer VernooΔ³</li>
142 <li>Copyright &copy; 2014, Jim Hague</li>
143 <li>Copyright &copy; 2014, Jim Hague</li>
143 <li>Copyright &copy; 2014, Matt Fellows</li>
144 <li>Copyright &copy; 2014, Matt Fellows</li>
144 <li>Copyright &copy; 2014, Max Roman</li>
145 <li>Copyright &copy; 2014, Max Roman</li>
145 <li>Copyright &copy; 2014, Na'Tosha Bard</li>
146 <li>Copyright &copy; 2014, Na'Tosha Bard</li>
146 <li>Copyright &copy; 2014, Rasmus Selsmark</li>
147 <li>Copyright &copy; 2014, Rasmus Selsmark</li>
147 <li>Copyright &copy; 2014, SkryabinD</li>
148 <li>Copyright &copy; 2014, SkryabinD</li>
148 <li>Copyright &copy; 2014, Tim Freund</li>
149 <li>Copyright &copy; 2014, Tim Freund</li>
149 <li>Copyright &copy; 2014, Travis Burtrum</li>
150 <li>Copyright &copy; 2014, Travis Burtrum</li>
150 <li>Copyright &copy; 2014, whosaysni</li>
151 <li>Copyright &copy; 2014, whosaysni</li>
151 <li>Copyright &copy; 2014, Zoltan Gyarmati</li>
152 <li>Copyright &copy; 2014, Zoltan Gyarmati</li>
152 <li>Copyright &copy; 2010&ndash;2013, Marcin KuΕΊmiΕ„ski</li>
153 <li>Copyright &copy; 2010&ndash;2013, Marcin KuΕΊmiΕ„ski</li>
153 <li>Copyright &copy; 2010&ndash;2013, RhodeCode GmbH</li>
154 <li>Copyright &copy; 2010&ndash;2013, RhodeCode GmbH</li>
154 <li>Copyright &copy; 2011, 2013, Aparkar</li>
155 <li>Copyright &copy; 2011, 2013, Aparkar</li>
155 <li>Copyright &copy; 2012&ndash;2013, Nemcio</li>
156 <li>Copyright &copy; 2012&ndash;2013, Nemcio</li>
156 <li>Copyright &copy; 2012&ndash;2013, xpol</li>
157 <li>Copyright &copy; 2012&ndash;2013, xpol</li>
157 <li>Copyright &copy; 2013, Andrey Mivrenik</li>
158 <li>Copyright &copy; 2013, Andrey Mivrenik</li>
158 <li>Copyright &copy; 2013, ArcheR</li>
159 <li>Copyright &copy; 2013, ArcheR</li>
159 <li>Copyright &copy; 2013, Dennis Brakhane</li>
160 <li>Copyright &copy; 2013, Dennis Brakhane</li>
160 <li>Copyright &copy; 2013, gnustavo</li>
161 <li>Copyright &copy; 2013, gnustavo</li>
161 <li>Copyright &copy; 2013, Grzegorz RoΕΌniecki</li>
162 <li>Copyright &copy; 2013, Grzegorz RoΕΌniecki</li>
162 <li>Copyright &copy; 2013, Ilya Beda</li>
163 <li>Copyright &copy; 2013, Ilya Beda</li>
163 <li>Copyright &copy; 2013, ivlevdenis</li>
164 <li>Copyright &copy; 2013, ivlevdenis</li>
164 <li>Copyright &copy; 2013, Jonathan Sternberg</li>
165 <li>Copyright &copy; 2013, Jonathan Sternberg</li>
165 <li>Copyright &copy; 2013, Leonardo Carneiro</li>
166 <li>Copyright &copy; 2013, Leonardo Carneiro</li>
166 <li>Copyright &copy; 2013, Magnus Ericmats</li>
167 <li>Copyright &copy; 2013, Magnus Ericmats</li>
167 <li>Copyright &copy; 2013, Martin Vium</li>
168 <li>Copyright &copy; 2013, Martin Vium</li>
168 <li>Copyright &copy; 2013, Mikhail Zholobov</li>
169 <li>Copyright &copy; 2013, Mikhail Zholobov</li>
169 <li>Copyright &copy; 2013, mokeev1995</li>
170 <li>Copyright &copy; 2013, mokeev1995</li>
170 <li>Copyright &copy; 2013, Ruslan Bekenev</li>
171 <li>Copyright &copy; 2013, Ruslan Bekenev</li>
171 <li>Copyright &copy; 2013, shirou - しろう</li>
172 <li>Copyright &copy; 2013, shirou - しろう</li>
172 <li>Copyright &copy; 2013, Simon Lopez</li>
173 <li>Copyright &copy; 2013, Simon Lopez</li>
173 <li>Copyright &copy; 2013, softforwinxp</li>
174 <li>Copyright &copy; 2013, softforwinxp</li>
174 <li>Copyright &copy; 2013, stephanj</li>
175 <li>Copyright &copy; 2013, stephanj</li>
175 <li>Copyright &copy; 2013, zhmylove</li>
176 <li>Copyright &copy; 2013, zhmylove</li>
176 <li>Copyright &copy; 2013, こいんとす</li>
177 <li>Copyright &copy; 2013, こいんとす</li>
177 <li>Copyright &copy; 2011&ndash;2012, Augusto Herrmann</li>
178 <li>Copyright &copy; 2011&ndash;2012, Augusto Herrmann</li>
178 <li>Copyright &copy; 2012, Dan Sheridan</li>
179 <li>Copyright &copy; 2012, Dan Sheridan</li>
179 <li>Copyright &copy; 2012, H Waldo G</li>
180 <li>Copyright &copy; 2012, H Waldo G</li>
180 <li>Copyright &copy; 2012, hppj</li>
181 <li>Copyright &copy; 2012, hppj</li>
181 <li>Copyright &copy; 2012, Indra Talip</li>
182 <li>Copyright &copy; 2012, Indra Talip</li>
182 <li>Copyright &copy; 2012, mikespook</li>
183 <li>Copyright &copy; 2012, mikespook</li>
183 <li>Copyright &copy; 2012, nansenat16</li>
184 <li>Copyright &copy; 2012, nansenat16</li>
184 <li>Copyright &copy; 2012, Philip Jameson</li>
185 <li>Copyright &copy; 2012, Philip Jameson</li>
185 <li>Copyright &copy; 2012, Raoul Thill</li>
186 <li>Copyright &copy; 2012, Raoul Thill</li>
186 <li>Copyright &copy; 2012, Tony Bussieres</li>
187 <li>Copyright &copy; 2012, Tony Bussieres</li>
187 <li>Copyright &copy; 2012, Vincent Duvert</li>
188 <li>Copyright &copy; 2012, Vincent Duvert</li>
188 <li>Copyright &copy; 2012, Vladislav Poluhin</li>
189 <li>Copyright &copy; 2012, Vladislav Poluhin</li>
189 <li>Copyright &copy; 2012, Zachary Auclair</li>
190 <li>Copyright &copy; 2012, Zachary Auclair</li>
190 <li>Copyright &copy; 2011, Ankit Solanki</li>
191 <li>Copyright &copy; 2011, Ankit Solanki</li>
191 <li>Copyright &copy; 2011, Dmitri Kuznetsov</li>
192 <li>Copyright &copy; 2011, Dmitri Kuznetsov</li>
192 <li>Copyright &copy; 2011, Jared Bunting</li>
193 <li>Copyright &copy; 2011, Jared Bunting</li>
193 <li>Copyright &copy; 2011, Jason Harris</li>
194 <li>Copyright &copy; 2011, Jason Harris</li>
194 <li>Copyright &copy; 2011, Les Peabody</li>
195 <li>Copyright &copy; 2011, Les Peabody</li>
195 <li>Copyright &copy; 2011, Liad Shani</li>
196 <li>Copyright &copy; 2011, Liad Shani</li>
196 <li>Copyright &copy; 2011, Lorenzo M. Catucci</li>
197 <li>Copyright &copy; 2011, Lorenzo M. Catucci</li>
197 <li>Copyright &copy; 2011, Matt Zuba</li>
198 <li>Copyright &copy; 2011, Matt Zuba</li>
198 <li>Copyright &copy; 2011, Nicolas VINOT</li>
199 <li>Copyright &copy; 2011, Nicolas VINOT</li>
199 <li>Copyright &copy; 2011, Shawn K. O'Shea</li>
200 <li>Copyright &copy; 2011, Shawn K. O'Shea</li>
200 <li>Copyright &copy; 2010, Łukasz Balcerzak</li>
201 <li>Copyright &copy; 2010, Łukasz Balcerzak</li>
201
202
202 ## We did not list the following copyright holders, given that they appeared
203 ## We did not list the following copyright holders, given that they appeared
203 ## to use for-profit company affiliations in their contribution in the
204 ## to use for-profit company affiliations in their contribution in the
204 ## Mercurial log and therefore I didn't know if copyright was theirs or
205 ## Mercurial log and therefore I didn't know if copyright was theirs or
205 ## their company's.
206 ## their company's.
206 ## Copyright &copy; 2011 Thayne Harbaugh <thayne@fusionio.com>
207 ## Copyright &copy; 2011 Thayne Harbaugh <thayne@fusionio.com>
207 ## Copyright &copy; 2012 Dies Koper <diesk@fast.au.fujitsu.com>
208 ## Copyright &copy; 2012 Dies Koper <diesk@fast.au.fujitsu.com>
208 ## Copyright &copy; 2012 Erwin Kroon <e.kroon@smartmetersolutions.nl>
209 ## Copyright &copy; 2012 Erwin Kroon <e.kroon@smartmetersolutions.nl>
209 ## Copyright &copy; 2012 Vincent Caron <vcaron@bearstech.com>
210 ## Copyright &copy; 2012 Vincent Caron <vcaron@bearstech.com>
210 ##
211 ##
211 ## These contributors' contributions may not be copyrightable:
212 ## These contributors' contributions may not be copyrightable:
212 ## philip.j@hostdime.com in 2012
213 ## philip.j@hostdime.com in 2012
213 ## Stefan Engel <mail@engel-stefan.de> in 2012
214 ## Stefan Engel <mail@engel-stefan.de> in 2012
214 ## Ton Plomp <tcplomp@gmail.com> in 2013
215 ## Ton Plomp <tcplomp@gmail.com> in 2013
215 ##
216 ##
216 </ul>
217 </ul>
217
218
218 <p>The above are the copyright holders who have submitted direct
219 <p>The above are the copyright holders who have submitted direct
219 contributions to the Kallithea repository.</p>
220 contributions to the Kallithea repository.</p>
220
221
221 <p>In the <a href="https://kallithea-scm.org/repos/kallithea">Kallithea
222 <p>In the <a href="https://kallithea-scm.org/repos/kallithea">Kallithea
222 source code</a>, there is a
223 source code</a>, there is a
223 <a href="https://kallithea-scm.org/repos/kallithea/files/tip/LICENSE.md">list
224 <a href="https://kallithea-scm.org/repos/kallithea/files/tip/LICENSE.md">list
224 of third-party libraries and code that Kallithea incorporates</a>.</p>
225 of third-party libraries and code that Kallithea incorporates</a>.</p>
225
226
226 <p>The front-end contains a <a href="${h.url('/LICENSES.txt')}">list of
227 <p>The front-end contains a <a href="${h.url('/LICENSES.txt')}">list of
227 software that is used to build the front-end</a> but isn't distributed as a
228 software that is used to build the front-end</a> but isn't distributed as a
228 part of Kallithea.</p>
229 part of Kallithea.</p>
229
230
230 </div>
231 </div>
231 </div>
232 </div>
232
233
233 </%def>
234 </%def>
General Comments 0
You need to be logged in to leave comments. Login now