Raise a http 404 error on paths that are too long
[pgweb/local.git] / pgweb / core / views.py
blobd9e871a98c6506fbc654b89d2446951c90ef5b21
1 from django.shortcuts import render, get_object_or_404
2 from django.http import HttpResponse, Http404, HttpResponseRedirect
3 from django.http import HttpResponseNotModified
4 from django.template import TemplateDoesNotExist, loader
5 from django.contrib.auth.decorators import user_passes_test
6 from pgweb.util.decorators import login_required
7 from django.contrib import messages
8 from django.views.decorators.csrf import csrf_exempt
9 from django.db.models import Count
10 from django.db import connection, transaction
11 from django.utils.http import http_date, parse_http_date
12 from django.conf import settings
13 import django
15 from datetime import date, datetime, timedelta
16 import os
17 import re
18 import urllib.parse
20 from pgweb.util.decorators import cache, nocache
21 from pgweb.util.contexts import render_pgweb, get_nav_menu, PGWebContextProcessor
22 from pgweb.util.helpers import simple_form, PgXmlHelper, HttpServerError
23 from pgweb.util.moderation import get_all_pending_moderations
24 from pgweb.util.misc import get_client_ip, varnish_purge, varnish_purge_expr, varnish_purge_xkey
25 from pgweb.util.sitestruct import get_all_pages_struct
27 # models needed for the pieces on the frontpage
28 from pgweb.news.models import NewsArticle, NewsTag
29 from pgweb.events.models import Event
30 from pgweb.quotes.models import Quote
31 from .models import Version, ImportedRSSItem
33 # models needed for the pieces on the community page
34 from pgweb.survey.models import Survey
36 # models and forms needed for core objects
37 from .models import Organisation
38 from .forms import OrganisationForm, MergeOrgsForm
41 # Front page view
42 @cache(minutes=10)
43 def home(request):
44 news = NewsArticle.objects.filter(approved=True)[:5]
45 today = date.today()
46 # get up to seven events to display on the homepage
47 event_base_queryset = Event.objects.select_related('country').filter(
48 approved=True,
49 enddate__gte=today,
51 # first, see if there are up to two non-badged events within 90 days
52 other_events = event_base_queryset.filter(
53 badged=False,
54 startdate__lte=today + timedelta(days=90),
55 ).order_by('enddate', 'startdate')[:2]
56 # based on that, get 7 - |other_events| community events to display
57 community_event_queryset = event_base_queryset.filter(badged=True).order_by('enddate', 'startdate')[:(7 - other_events.count())]
58 # now, return all the events in one unioned array!
59 events = community_event_queryset.union(other_events).order_by('enddate', 'startdate').all()
60 versions = Version.objects.filter(supported=True)
61 planet = ImportedRSSItem.objects.filter(feed__internalname="planet").order_by("-posttime")[:9]
63 return render(request, 'index.html', {
64 'title': 'The world\'s most advanced open source database',
65 'news': news,
66 'newstags': NewsTag.objects.all(),
67 'events': events,
68 'versions': versions,
69 'planet': planet,
73 # About page view (contains information about PostgreSQL + random quotes)
74 @cache(minutes=10)
75 def about(request):
76 # get 5 random quotes
77 quotes = Quote.objects.filter(approved=True).order_by('?').all()[:5]
78 return render_pgweb(request, 'about', 'core/about.html', {
79 'quotes': quotes,
83 # Community main page (contains surveys and potentially more)
84 def community(request):
85 s = Survey.objects.filter(current=True)
86 try:
87 s = s[0]
88 except:
89 s = None
90 planet = ImportedRSSItem.objects.filter(feed__internalname="planet").order_by("-posttime")[:7]
91 return render_pgweb(request, 'community', 'core/community.html', {
92 'survey': s,
93 'planet': planet,
97 # List of supported versions
98 def versions(request):
99 return render_pgweb(request, 'support', 'support/versioning.html', {
100 'versions': Version.objects.filter(tree__gt=0).filter(testing=0),
104 re_staticfilenames = re.compile("^[0-9A-Z/_-]+$", re.IGNORECASE)
107 # Generic fallback view for static pages
108 def fallback(request, url):
109 if url.find('..') > -1:
110 raise Http404('Page not found.')
112 if not re_staticfilenames.match(url):
113 raise Http404('Page not found.')
115 if len(url) > 250:
116 # Maximum length is really per-directory, but we shouldn't have any pages/fallback
117 # urls with anywhere *near* that, so let's just limit it on the whole
118 raise Http404('Page not found.')
120 try:
121 t = loader.get_template('pages/%s.html' % url)
122 except TemplateDoesNotExist:
123 try:
124 t = loader.get_template('pages/%s/en.html' % url)
125 except TemplateDoesNotExist:
126 raise Http404('Page not found.')
128 # Guestimate the nav section by looking at the URL and taking the first
129 # piece of it.
130 try:
131 navsect = url.split('/', 2)[0]
132 except:
133 navsect = ''
134 c = PGWebContextProcessor(request)
135 c.update({'navmenu': get_nav_menu(navsect)})
136 return HttpResponse(t.render(c))
139 # Edit-forms for core objects
140 @login_required
141 def organisationform(request, itemid):
142 if itemid != 'new':
143 get_object_or_404(Organisation, pk=itemid, managers=request.user)
145 return simple_form(Organisation, itemid, request, OrganisationForm,
146 redirect='/account/edit/organisations/')
149 # robots.txt
150 def robots(request):
151 return HttpResponse("""User-agent: *
152 Disallow: /admin/
153 Disallow: /account/
154 Disallow: /docs/devel/
155 Disallow: /list/
156 Disallow: /search/
157 Disallow: /message-id/raw/
158 Disallow: /message-id/flat/
160 Sitemap: https://www.postgresql.org/sitemap.xml
161 """, content_type='text/plain')
164 def _make_sitemap(pagelist):
165 resp = HttpResponse(content_type='text/xml')
166 x = PgXmlHelper(resp)
167 x.startDocument()
168 x.startElement('urlset', {'xmlns': 'http://www.sitemaps.org/schemas/sitemap/0.9'})
169 pages = 0
170 for p in pagelist:
171 pages += 1
172 x.startElement('url', {})
173 x.add_xml_element('loc', 'https://www.postgresql.org/%s' % urllib.parse.quote(p[0]))
174 if len(p) > 1 and p[1]:
175 x.add_xml_element('priority', str(p[1]))
176 if len(p) > 2 and p[2]:
177 x.add_xml_element('lastmod', p[2].isoformat() + "Z")
178 x.endElement('url')
179 x.endElement('urlset')
180 x.endDocument()
181 return resp
184 # Sitemap (XML format)
185 @cache(hours=6)
186 def sitemap(request):
187 return _make_sitemap(get_all_pages_struct())
190 # Internal sitemap (only for our own search engine)
191 # Note! Still served up to anybody who wants it, so don't
192 # put anything secret in it...
193 @cache(hours=6)
194 def sitemap_internal(request):
195 return _make_sitemap(get_all_pages_struct(method='get_internal_struct'))
198 # dynamic CSS serving, meaning we merge a number of different CSS into a
199 # single one, making sure it turns into a single http response. We do this
200 # dynamically, since the output will be cached.
201 _dynamic_cssmap = {
202 'base': ['media/css/main.css',
203 'media/css/normalize.css', ],
204 'docs': ['media/css/global.css',
205 'media/css/table.css',
206 'media/css/text.css',
207 'media/css/docs.css'],
211 @cache(hours=6)
212 def dynamic_css(request, css):
213 if css not in _dynamic_cssmap:
214 raise Http404('CSS not found')
215 files = _dynamic_cssmap[css]
216 resp = HttpResponse(content_type='text/css')
218 # We honor if-modified-since headers by looking at the most recently
219 # touched CSS file.
220 latestmod = 0
221 for fn in files:
222 try:
223 stime = os.stat(fn).st_mtime
224 if latestmod < stime:
225 latestmod = stime
226 except OSError:
227 # If we somehow referred to a file that didn't exist, or
228 # one that we couldn't access.
229 raise Http404('CSS (sub) not found')
230 if 'HTTP_IF_MODIFIED_SINCE' in request.META:
231 # This code is mostly stolen from django :)
232 matches = re.match(r"^([^;]+)(; length=([0-9]+))?$",
233 request.META.get('HTTP_IF_MODIFIED_SINCE'),
234 re.IGNORECASE)
235 header_mtime = parse_http_date(matches.group(1))
236 # We don't do length checking, just the date
237 if int(latestmod) <= header_mtime:
238 return HttpResponseNotModified(content_type='text/css')
239 resp['Last-Modified'] = http_date(latestmod)
241 for fn in files:
242 with open(fn) as f:
243 resp.write("/* %s */\n" % fn)
244 resp.write(f.read())
245 resp.write("\n")
247 return resp
250 @nocache
251 def csrf_failure(request, reason=''):
252 resp = render(request, 'errors/csrf_failure.html', {
253 'reason': reason,
255 resp.status_code = 403 # Forbidden
256 return resp
259 # Basic information about the connection
260 @cache(seconds=30)
261 def system_information(request):
262 return render(request, 'core/system_information.html', {
263 'server': os.uname()[1],
264 'cache_server': request.META['REMOTE_ADDR'] or None,
265 'client_ip': get_client_ip(request),
266 'django_version': django.get_version(),
270 # Sync timestamp for automirror. Keep it around for 30 seconds
271 # Basically just a check that we can access the backend still...
272 @cache(seconds=30)
273 def sync_timestamp(request):
274 s = datetime.now().strftime("%Y-%m-%d %H:%M:%S\n")
275 r = HttpResponse(s, content_type='text/plain')
276 r['Content-Length'] = len(s)
277 return r
280 # List of all unapproved objects, for the special admin page
281 @login_required
282 @user_passes_test(lambda u: u.is_staff)
283 @user_passes_test(lambda u: u.groups.filter(name='pgweb moderators').exists())
284 def admin_pending(request):
285 return render(request, 'core/admin_pending.html', {
286 'app_list': get_all_pending_moderations(),
290 # Purge objects from varnish, for the admin pages
291 @login_required
292 @user_passes_test(lambda u: u.is_staff)
293 @user_passes_test(lambda u: u.groups.filter(name='varnish purgers').exists())
294 def admin_purge(request):
295 if request.method == 'POST':
296 url = request.POST['url']
297 expr = request.POST['expr']
298 xkey = request.POST['xkey']
299 l = len([_f for _f in [url, expr, xkey] if _f])
300 if l == 0:
301 # Nothing specified
302 return HttpResponseRedirect('.')
303 elif l > 1:
304 messages.error(request, "Can only specify one of url, expression and xkey!")
305 return HttpResponseRedirect('.')
307 if url:
308 varnish_purge(url)
309 elif expr:
310 varnish_purge_expr(expr)
311 else:
312 varnish_purge_xkey(xkey)
314 messages.info(request, "Purge added.")
315 return HttpResponseRedirect('.')
317 # Fetch list of latest purges
318 curs = connection.cursor()
319 curs.execute("SELECT added, completed, consumer, CASE WHEN mode = 'K' THEN 'XKey' WHEN mode='P' THEN 'URL' ELSE 'Expression' END, expr FROM varnishqueue.queue q LEFT JOIN varnishqueue.consumers c ON c.consumerid=q.consumerid ORDER BY added DESC")
320 latest = curs.fetchall()
322 return render(request, 'core/admin_purge.html', {
323 'latest_purges': latest,
327 @csrf_exempt
328 def api_varnish_purge(request):
329 if not request.META['REMOTE_ADDR'] in settings.VARNISH_PURGERS:
330 return HttpServerError(request, "Invalid client address")
331 if request.method != 'POST':
332 return HttpServerError(request, "Can't use this way")
333 n = int(request.POST['n'])
334 curs = connection.cursor()
335 for i in range(0, n):
336 if 'p{0}'.format(i) in request.POST:
337 curs.execute("SELECT varnish_purge_expr(%s)", (request.POST['p{0}'.format(i)], ))
338 if 'x{0}'.format(i) in request.POST:
339 curs.execute("SELECT varnish_purge_xkey(%s)", (request.POST['x{0}'.format(i)], ))
341 return HttpResponse("Purged %s entries\n" % n)
344 # Merge two organisations
345 @login_required
346 @user_passes_test(lambda u: u.is_superuser)
347 @transaction.atomic
348 def admin_mergeorg(request):
349 if request.method == 'POST':
350 form = MergeOrgsForm(data=request.POST)
351 if form.is_valid():
352 # Ok, try to actually merge organisations, by moving all objects
353 # attached
354 f = form.cleaned_data['merge_from']
355 t = form.cleaned_data['merge_into']
356 for e in f.event_set.all():
357 e.org = t
358 e.save()
359 for n in f.newsarticle_set.all():
360 n.org = t
361 n.save()
362 for p in f.product_set.all():
363 p.org = t
364 p.save()
365 for p in f.professionalservice_set.all():
366 p.organisation = t
367 p.save()
368 # Now that everything is moved, we can delete the organisation
369 f.delete()
371 return HttpResponseRedirect("/admin/core/organisation/")
372 # Else fall through to re-render form with errors
373 else:
374 form = MergeOrgsForm()
376 return render(request, 'core/admin_mergeorg.html', {
377 'form': form,