~andreserl/+junk/cobbler

« back to all changes in this revision

Viewing changes to debian/patches/59_add_csrf_protection.patch

  • Committer: Andres Rodriguez
  • Date: 2011-12-09 17:39:33 UTC
  • mfrom: (50.1.5 trunk)
  • Revision ID: andreserl@ubuntu.com-20111209173933-6mel1k0noqjd1vad
Tags: 2.1.0+git20110602-0ubuntu26.2
* SECURITY UPDATE: arbitrary code execution via PYTHON_EGG_CACHE in insecure
  location (LP: #858875)
  - debian/patches/58_fix_egg_cache.patch: move PYTHON_EGG_CACHE to
    /var/lib/cobbler/webui_cache (copied from fix to precise).
* SECURITY UPDATE: CSRF vulnerability in cobbler-web (LP: #858878)
  - debian/patches/59_add_csrf_protection.patch: use Django's built-in
    CSRF protection (taken from upstream).
* SECURITY UPDATE: arbitrary code execution via web interface (LP: #858883)
  - debian/patches/60_yaml_safe_load.patch: use yaml.safe_load instead of
    yaml.load (taken from upstream).
* SECURITY UPDATE: users.digest file is world readable (LP: #858860)
  - debian/cobbler.postinst: create /etc/cobbler/users.digest as 600
* SECURITY UPDATE: webui_sessions uses insecure permissions (LP: #863755)
  - debian/cobbler.postinst: fix permissions on webui_{sessions,cache} to
    0700

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
Author: James Cammarata <jimi@sngx.net>
 
2
Description: Add CSRF protection to cobbler-web
 
3
Bug-Ubuntu: https://launchpad.net/bugs/858878
 
4
Origin: backport, http://git.fedorahosted.org/git/?p=cobbler;a=commit;h=18eb1c06779b37d89dfb2962a08236dd1bab24a6
 
5
 
 
6
This also includes previous CSRF work from http://git.fedorahosted.org/git?p=cobbler;a=commit;h=4bee30b4086a8d845bea5d39d6f2cba1f4a396aa
 
7
 
 
8
--- a/web/cobbler_web/templates/generic_edit.tmpl
 
9
+++ b/web/cobbler_web/templates/generic_edit.tmpl
 
10
@@ -370,6 +370,7 @@
 
11
 <h1>{% ifequal editmode 'edit'  %}Editing{% else %}Adding{% endifequal %} a {{ what|capfirst }}{%ifequal editmode 'edit' %}: {{ name }}{% endifequal %}</h1>
 
12
 <hr />
 
13
 <form method="post" action="/cobbler_web/{{ what }}/save">
 
14
+  {% csrf_token %}
 
15
   <input type="hidden" name="editmode" value="{{ editmode }}" />
 
16
   <input type="hidden" name="subobject" value="{{ subobject }}" />
 
17
   <ol>
 
18
--- a/web/cobbler_web/templates/generic_list.tmpl
 
19
+++ b/web/cobbler_web/templates/generic_list.tmpl
 
20
@@ -2,6 +2,7 @@
 
21
 {% load site %}
 
22
 {% block content %}
 
23
 
 
24
+<form id="action" method="POST" action="">{% csrf_token %}</form>
 
25
 <script type="text/javascript">
 
26
 function items_check_all(){
 
27
     var checkall = document.getElementById("itemsall").checked
 
28
@@ -31,18 +32,21 @@
 
29
 function obj_rename(old) {
 
30
     var newname = window.prompt("Change {{ what }} name to?",old);
 
31
     if (newname != null) {
 
32
-        window.location = "/cobbler_web/{{ what }}/rename/" + old + "/" + newname;
 
33
+        document.forms["action"].action = "/cobbler_web/{{ what }}/rename/" + old + "/" + newname;
 
34
+        document.forms["action"].submit();
 
35
     }
 
36
 }
 
37
 function obj_copy(old) {
 
38
     var newname = window.prompt("Name for the new {{ what }}?",old);
 
39
     if (newname != null) {
 
40
-        window.location = "/cobbler_web/{{ what }}/copy/" + old + "/" + newname;
 
41
+        document.forms["action"].action = "/cobbler_web/{{ what }}/copy/" + old + "/" + newname;
 
42
+        document.forms["action"].submit();
 
43
     }
 
44
 }
 
45
 function obj_delete(old) {
 
46
     if (confirm("Delete {{ what }} (" + old + ") and all child objects?")) {
 
47
-        window.location = "/cobbler_web/{{ what }}/delete/" + old;
 
48
+        document.forms["action"].action = "/cobbler_web/{{ what }}/delete/" + old;
 
49
+        document.forms["action"].submit();
 
50
     }
 
51
 }
 
52
 
 
53
@@ -53,6 +57,11 @@
 
54
     document.location = "/cobbler_web/" + what + "/" + action
 
55
 }
 
56
 
 
57
+function action_sort(value) {
 
58
+    document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/sort/' + value;
 
59
+    document.forms["action"].submit();
 
60
+}
 
61
+
 
62
 function action_multi(otype) {
 
63
     var values = items_checked_values()
 
64
     if (values == "") {
 
65
@@ -123,6 +132,7 @@
 
66
 </ul>
 
67
 
 
68
 <form name="myform" method="post" action="/cobbler_web/{{ what }}/action">
 
69
+  {% csrf_token %}
 
70
   <table id="listitems" cellspacing="0">
 
71
     <thead>
 
72
       <tr>
 
73
@@ -131,7 +141,7 @@
 
74
         </th>
 
75
 {% for value in columns %}
 
76
         <th>
 
77
-          <a href="/cobbler_web/{{ what }}/modifylist/sort/{{ value.0 }}">{{ value.0|title }}</a>
 
78
+          <a href="javascript:action_sort('{{ value.0 }}');">{{ value.0|title }}</a>
 
79
   {% ifequal value.1 "asc" %}
 
80
           &darr;
 
81
   {% endifequal %}
 
82
--- a/web/cobbler_web/templates/import.tmpl
 
83
+++ b/web/cobbler_web/templates/import.tmpl
 
84
@@ -3,6 +3,7 @@
 
85
 <h1>DVD Importer</h1>
 
86
 <hr />
 
87
 <form method="post" action="/cobbler_web/import/run">
 
88
+  {% csrf_token %}
 
89
   <div class="sectionbody">
 
90
   <ul>
 
91
     <li class="editrow">
 
92
--- a/web/cobbler_web/templates/ksfile_edit.tmpl
 
93
+++ b/web/cobbler_web/templates/ksfile_edit.tmpl
 
94
@@ -17,6 +17,7 @@
 
95
 <h1>{% ifequal editmode 'edit' %}Editing{% else %}Adding{% endifequal %} a Kickstart Template</h1>
 
96
 <hr />
 
97
 <form id="ksform" method="post" action="/cobbler_web/ksfile/save">
 
98
+  {% csrf_token %}
 
99
   <ol>
 
100
     <li>
 
101
       <label for="ksdata">{% if ksfile_name %}Editing: {{ ksfile_name }}{% else %}Filename:{% endif %}</label>
 
102
--- a/web/cobbler_web/templates/login.tmpl
 
103
+++ b/web/cobbler_web/templates/login.tmpl
 
104
@@ -13,6 +13,7 @@
 
105
     <p class="error">Sorry, that's not a valid username or password</p>
 
106
 {% endif %}
 
107
     <form action="/cobbler_web/do_login" method="post">
 
108
+      {% csrf_token %}
 
109
       {% if next %}<input type="hidden" name="next" value="{{ next|escape }}" />{% endif %}
 
110
       <div id="username">
 
111
         <label for="username">Username: </label>
 
112
--- a/web/cobbler_web/templates/snippet_edit.tmpl
 
113
+++ b/web/cobbler_web/templates/snippet_edit.tmpl
 
114
@@ -14,6 +14,7 @@
 
115
 <h1>{% ifequal editmode 'edit' %}Editing{% else %}Adding{% endifequal %} a Snippet</h1>
 
116
 <hr />
 
117
 <form id="snippetform" method="post" action="/cobbler_web/snippet/save">
 
118
+  {% csrf_token %}
 
119
   <ol>
 
120
     <li>
 
121
       <label for="snippetdata">{% if snippet_name %}Snippet: {{ snippet_name }}{% else %}Filename:{% endif %}</label>
 
122
--- a/web/cobbler_web/views.py
 
123
+++ b/web/cobbler_web/views.py
 
124
@@ -4,6 +4,26 @@
 
125
 from django.http import HttpResponse
 
126
 from django.http import HttpResponseRedirect
 
127
 from django.shortcuts import render_to_response
 
128
+from django.views.decorators.http import require_POST
 
129
+
 
130
+try:
 
131
+    from django.views.decorators.csrf import csrf_protect
 
132
+except:
 
133
+    # Old Django, fudge the @csrf_protect decorator to be a pass-through 
 
134
+    # that does nothing. Django decorator shell based on this page: 
 
135
+    # http://passingcuriosity.com/2009/writing-view-decorators-for-django/
 
136
+    def csrf_protect(f):
 
137
+        def _dec(view_func):
 
138
+            def _view(request,*args,**kwargs):
 
139
+                return view_func(request,*args,**kwargs)
 
140
+            _view.__name__ = view_func.__name__
 
141
+            _view.__dict__ = view_func.__dict__
 
142
+            _view.__doc__  = view_func.__doc__
 
143
+            return _view
 
144
+        if f is None:
 
145
+            return _dec
 
146
+        else:
 
147
+            return _dec(f)
 
148
 
 
149
 import xmlrpclib
 
150
 import time
 
151
@@ -37,7 +57,7 @@
 
152
    if not test_user_authenticated(request): return login(request,next="/cobbler_web")
 
153
 
 
154
    t = get_template('index.tmpl')
 
155
-   html = t.render(Context({
 
156
+   html = t.render(RequestContext(request,{
 
157
         'version' : remote.version(request.session['token']),
 
158
         'username': username,
 
159
    }))
 
160
@@ -51,7 +71,7 @@
 
161
    """
 
162
    if not test_user_authenticated(request): return login(request)
 
163
    t = get_template("task_created.tmpl")
 
164
-   html = t.render(Context({
 
165
+   html = t.render(RequestContext(request,{
 
166
        'version'  : remote.version(request.session['token']),
 
167
        'username' : username
 
168
    }))
 
169
@@ -69,7 +89,7 @@
 
170
    t = get_template('error_page.tmpl')
 
171
    message = message.replace("<Fault 1: \"<class 'cobbler.cexceptions.CX'>:'","Remote exception: ")
 
172
    message = message.replace("'\">","")
 
173
-   html = t.render(Context({
 
174
+   html = t.render(RequestContext(request,{
 
175
        'version' : remote.version(request.session['token']),
 
176
        'message' : message,
 
177
        'username': username
 
178
@@ -340,7 +360,8 @@
 
179
     }))
 
180
     return HttpResponse(html)
 
181
 
 
182
-
 
183
+@require_POST
 
184
+@csrf_protect
 
185
 def modify_list(request, what, pref, value=None):
 
186
     """
 
187
     This function is used in the generic list view
 
188
@@ -407,6 +428,8 @@
 
189
 
 
190
 # ======================================================================
 
191
 
 
192
+@require_POST
 
193
+@csrf_protect
 
194
 def generic_rename(request, what, obj_name=None, obj_newname=None):
 
195
 
 
196
    """
 
197
@@ -427,6 +450,8 @@
 
198
 
 
199
 # ======================================================================
 
200
 
 
201
+@require_POST
 
202
+@csrf_protect
 
203
 def generic_copy(request, what, obj_name=None, obj_newname=None):
 
204
    """
 
205
    Copies an object.
 
206
@@ -446,6 +471,8 @@
 
207
 
 
208
 # ======================================================================
 
209
 
 
210
+@require_POST
 
211
+@csrf_protect
 
212
 def generic_delete(request, what, obj_name=None):
 
213
    """
 
214
    Deletes an object.
 
215
@@ -465,6 +492,8 @@
 
216
 
 
217
 # ======================================================================
 
218
 
 
219
+@require_POST
 
220
+@csrf_protect
 
221
 def generic_domulti(request, what, multi_mode=None, multi_arg=None):
 
222
 
 
223
     """
 
224
@@ -527,7 +556,7 @@
 
225
 def import_prompt(request):
 
226
    if not test_user_authenticated(request): return login(request)
 
227
    t = get_template('import.tmpl')
 
228
-   html = t.render(Context({
 
229
+   html = t.render(RequestContext(request,{
 
230
        'version'  : remote.version(request.session['token']),
 
231
        'username' : username,
 
232
    }))
 
233
@@ -542,7 +571,7 @@
 
234
    if not test_user_authenticated(request): return login(request)
 
235
    results = remote.check(request.session['token'])
 
236
    t = get_template('check.tmpl')
 
237
-   html = t.render(Context({
 
238
+   html = t.render(RequestContext(request,{
 
239
        'version': remote.version(request.session['token']),
 
240
        'username' : username,
 
241
        'results'  : results
 
242
@@ -551,6 +580,8 @@
 
243
 
 
244
 # ======================================================================
 
245
 
 
246
+@require_POST
 
247
+@csrf_protect
 
248
 def buildiso(request):
 
249
     if not test_user_authenticated(request): return login(request)
 
250
     remote.background_buildiso({},request.session['token'])
 
251
@@ -558,6 +589,8 @@
 
252
 
 
253
 # ======================================================================
 
254
 
 
255
+@require_POST
 
256
+@csrf_protect
 
257
 def import_run(request):
 
258
     if not test_user_authenticated(request): return login(request)
 
259
     options = {
 
260
@@ -588,7 +621,7 @@
 
261
          ksfile_list.append((ksfile,ksfile,None))
 
262
 
 
263
    t = get_template('ksfile_list.tmpl')
 
264
-   html = t.render(Context({
 
265
+   html = t.render(RequestContext(request,{
 
266
        'what':'ksfile',
 
267
        'ksfiles': ksfile_list,
 
268
        'version': remote.version(request.session['token']),
 
269
@@ -599,7 +632,7 @@
 
270
 
 
271
 # ======================================================================
 
272
 
 
273
-
 
274
+@csrf_protect
 
275
 def ksfile_edit(request, ksfile_name=None, editmode='edit'):
 
276
    """
 
277
    This is the page where a kickstart file is edited.
 
278
@@ -617,7 +650,7 @@
 
279
       ksdata = remote.read_or_write_kickstart_template(ksfile_name, True, "", request.session['token'])
 
280
 
 
281
    t = get_template('ksfile_edit.tmpl')
 
282
-   html = t.render(Context({
 
283
+   html = t.render(RequestContext(request,{
 
284
        'ksfile_name' : ksfile_name,
 
285
        'deleteable'  : deleteable,
 
286
        'ksdata'      : ksdata,
 
287
@@ -630,6 +663,8 @@
 
288
 
 
289
 # ======================================================================
 
290
 
 
291
+@require_POST
 
292
+@csrf_protect
 
293
 def ksfile_save(request):
 
294
    """
 
295
    This page processes and saves edits to a kickstart file.
 
296
@@ -673,7 +708,7 @@
 
297
          snippet_list.append((snippet,snippet,None))
 
298
 
 
299
    t = get_template('snippet_list.tmpl')
 
300
-   html = t.render(Context({
 
301
+   html = t.render(RequestContext(request,{
 
302
        'what'     : 'snippet',
 
303
        'snippets' : snippet_list,
 
304
        'version'  : remote.version(request.session['token']),
 
305
@@ -683,6 +718,7 @@
 
306
 
 
307
 # ======================================================================
 
308
 
 
309
+@csrf_protect
 
310
 def snippet_edit(request, snippet_name=None, editmode='edit'):
 
311
    """
 
312
    This page edits a specific snippet.
 
313
@@ -700,7 +736,7 @@
 
314
       snippetdata = remote.read_or_write_snippet(snippet_name, True, "", request.session['token'])
 
315
 
 
316
    t = get_template('snippet_edit.tmpl')
 
317
-   html = t.render(Context({
 
318
+   html = t.render(RequestContext(request,{
 
319
        'snippet_name' : snippet_name,
 
320
        'deleteable'   : deleteable,
 
321
        'snippetdata'  : snippetdata,
 
322
@@ -713,6 +749,8 @@
 
323
 
 
324
 # ======================================================================
 
325
 
 
326
+@require_POST
 
327
+@csrf_protect
 
328
 def snippet_save(request):
 
329
    """
 
330
    This snippet saves a snippet once edited.
 
331
@@ -755,7 +793,7 @@
 
332
       results.append([k,settings[k]])
 
333
 
 
334
    t = get_template('settings.tmpl')
 
335
-   html = t.render(Context({
 
336
+   html = t.render(RequestContext(request,{
 
337
         'settings' : results,
 
338
         'version'  : remote.version(request.session['token']),
 
339
         'username' : username,
 
340
@@ -781,7 +819,7 @@
 
341
    events2.sort(sorter)
 
342
 
 
343
    t = get_template('events.tmpl')
 
344
-   html = t.render(Context({
 
345
+   html = t.render(RequestContext(request,{
 
346
        'results'  : events2,
 
347
        'version'  : remote.version(request.session['token']),
 
348
        'username' : username
 
349
@@ -815,7 +853,7 @@
 
350
       'version'    : remote.version(request.session['token']),
 
351
       'username'  : username
 
352
    }
 
353
-   html = t.render(Context(vars))
 
354
+   html = t.render(RequestContext(request,vars))
 
355
    return HttpResponse(html)
 
356
 
 
357
 # ======================================================================
 
358
@@ -831,6 +869,8 @@
 
359
 
 
360
 # ======================================================================
 
361
 
 
362
+@require_POST
 
363
+@csrf_protect
 
364
 def sync(request):
 
365
    """
 
366
    Runs 'cobbler sync' from the API when the user presses the sync button.
 
367
@@ -841,6 +881,8 @@
 
368
 
 
369
 # ======================================================================
 
370
 
 
371
+@require_POST
 
372
+@csrf_protect
 
373
 def reposync(request):
 
374
    """
 
375
    Syncs all repos that are configured to be synced.
 
376
@@ -851,6 +893,8 @@
 
377
 
 
378
 # ======================================================================
 
379
 
 
380
+@require_POST
 
381
+@csrf_protect
 
382
 def hardlink(request):
 
383
    """
 
384
    Hardlinks files between repos and install trees to save space.
 
385
@@ -861,6 +905,8 @@
 
386
 
 
387
 # ======================================================================
 
388
 
 
389
+@require_POST
 
390
+@csrf_protect
 
391
 def replicate(request):
 
392
    """
 
393
    Replicate configuration from the central cobbler server, configured
 
394
@@ -893,6 +939,7 @@
 
395
 
 
396
 # ======================================================================
 
397
 
 
398
+@csrf_protect
 
399
 def generic_edit(request, what=None, obj_name=None, editmode="new"):
 
400
 
 
401
    """
 
402
@@ -958,7 +1005,7 @@
 
403
    t = get_template('generic_edit.tmpl')
 
404
    inames = interfaces.keys()
 
405
    inames.sort()
 
406
-   html = t.render(Context({
 
407
+   html = t.render(RequestContext(request,{
 
408
        'what'            : what, 
 
409
        'fields'          : fields, 
 
410
        'subobject'       : child,
 
411
@@ -976,6 +1023,8 @@
 
412
 
 
413
 # ======================================================================
 
414
 
 
415
+@require_POST
 
416
+@csrf_protect
 
417
 def generic_save(request,what):
 
418
 
 
419
     """
 
420
@@ -1124,9 +1173,12 @@
 
421
             pass
 
422
     return False
 
423
 
 
424
+@csrf_protect
 
425
 def login(request, next=None):
 
426
-    return render_to_response('login.tmpl', {'next':next})
 
427
+    return render_to_response('login.tmpl', RequestContext(request,{'next':next}))
 
428
 
 
429
+@require_POST
 
430
+@csrf_protect
 
431
 def do_login(request):
 
432
     global remote
 
433
     global username
 
434
@@ -1156,6 +1208,8 @@
 
435
     else:
 
436
         return login(request,nextsite)
 
437
 
 
438
+@require_POST
 
439
+@csrf_protect
 
440
 def do_logout(request):
 
441
     request.session['username'] = ""
 
442
     request.session['token'] = ""
 
443
--- a/web/settings.py
 
444
+++ b/web/settings.py
 
445
@@ -1,4 +1,5 @@
 
446
 # Django settings for cobbler-web project.
 
447
+import django
 
448
 
 
449
 DEBUG = True
 
450
 TEMPLATE_DEBUG = DEBUG
 
451
@@ -42,6 +43,7 @@
 
452
 )
 
453
 MIDDLEWARE_CLASSES = (
 
454
     'django.middleware.common.CommonMiddleware',
 
455
+    'django.middleware.csrf.CsrfViewMiddleware',
 
456
     'django.contrib.sessions.middleware.SessionMiddleware',
 
457
     'django.contrib.auth.middleware.AuthenticationMiddleware',
 
458
 )
 
459
--- a/web/cobbler_web/templates/filter.tmpl
 
460
+++ b/web/cobbler_web/templates/filter.tmpl
 
461
@@ -1,5 +1,6 @@
 
462
 {% if pageinfo %}
 
463
 <script type="text/javascript">
 
464
+// these functions depend on the "action" form defined in generic_list
 
465
 function add_filter() {
 
466
     field_name = document.getElementById("filter_field");
 
467
     field_value = document.getElementById("filter_value");
 
468
@@ -8,9 +9,14 @@
 
469
     } else if (field_value.value == "") {
 
470
         alert("You must select a filter value.");
 
471
     } else {
 
472
-        location = '/cobbler_web/{{ what }}/modifylist/addfilter/'+field_name.value+':'+field_value.value;
 
473
+        document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/addfilter/'+field_name.value+':'+field_value.value;
 
474
+        document.forms["action"].submit();
 
475
     }
 
476
 }
 
477
+function del_filter(filter) {
 
478
+    document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/removefilter/' + filter
 
479
+    document.forms["action"].submit();
 
480
+}
 
481
 </script>
 
482
 <ul id="filter-adder">
 
483
   <li>
 
484
@@ -148,7 +154,7 @@
 
485
   {% if filters %}
 
486
 <ul id="filter-remover">
 
487
     {% for key,value in filters.items %}
 
488
-  <li><a href="/cobbler_web/{{ what }}/modifylist/removefilter/{{ key }}" title="remove">✖</a> {{ key }} = {{ value }}</li>
 
489
+  <li><a href="javascript:del_filter('{{ key }}');" title="remove">✖</a> {{ key }} = {{ value }}</li>
 
490
     {% endfor %}
 
491
 </ul>
 
492
   {% endif %}
 
493
--- a/web/cobbler_web/templates/master.tmpl
 
494
+++ b/web/cobbler_web/templates/master.tmpl
 
495
@@ -14,10 +14,17 @@
 
496
 <div id="container">
 
497
   <div id='user'>
 
498
     <input type="hidden" name="username" id="username" value="{{ username }}" />
 
499
-    Logged in: <b>{{ username }}</b> <a class="action" href="/cobbler_web/logout">Logout</a>
 
500
+    Logged in: <b>{{ username }}</b> <a class="action" href="javascript:menuaction('/cobbler_web/logout');">Logout</a>
 
501
   </div>
 
502
     <div id="menubar">
 
503
       <big><b>Orchestra</b></big><br/><i>powered by <a href="https://fedorahosted.org/cobbler/">Cobbler</a></i><br/><br/>
 
504
+      <form id="menuaction" method="POST" action="">{% csrf_token %}</form>
 
505
+      <script type="text/javascript">
 
506
+      function menuaction(action) {
 
507
+         document.forms["menuaction"].action = action
 
508
+         document.forms["menuaction"].submit();
 
509
+      }
 
510
+      </script>
 
511
       <h1>Configuration</h1>
 
512
       <ul>
 
513
         <li><a href="/cobbler_web/distro/list" class="edit">Distros</a></li>
 
514
@@ -37,11 +44,11 @@
 
515
       <h1>Actions</h1>
 
516
       <ul>
 
517
         <li><a href="/cobbler_web/import/prompt">Import DVD</a></li>
 
518
-        <li><a href="/cobbler_web/sync">Sync</a> ☼</li>
 
519
-        <li><a href="/cobbler_web/reposync">Reposync</a> ☼ </li>
 
520
-        <li><a href="/cobbler_web/hardlink">Hardlink</a> ☼ </li>
 
521
-        <!-- <li><a href="/cobbler_web/replicate">Replicate</a> ☼ </li> -->
 
522
-        <li><a href="/cobbler_web/buildiso">Build ISO</a> ☼ </li>
 
523
+        <li><a href="javascript:menuaction('/cobbler_web/sync');">Sync</a> ☼</li>
 
524
+        <li><a href="javascript:menuaction('/cobbler_web/reposync');"">Reposync</a> ☼ </li>
 
525
+        <li><a href="javascript:menuaction('/cobbler_web/hardlink');"">Hardlink</a> ☼ </li>
 
526
+        <!-- <li><a href="javascript:menuaction('/cobbler_web/replicate');"">Replicate</a> ☼ </li> -->
 
527
+        <li><a href="javascript:menuaction('/cobbler_web/buildiso');">Build ISO</a> ☼ </li>
 
528
       </ul>
 
529
       <h1>Cobbler</h1>
 
530
       <ul>
 
531
--- a/web/cobbler_web/templates/paginate.tmpl
 
532
+++ b/web/cobbler_web/templates/paginate.tmpl
 
533
@@ -1,20 +1,32 @@
 
534
 {% if pageinfo %}
 
535
+<script type="text/javascript">
 
536
+// All of these functions depend on the form "action" defined in generic_list
 
537
+function change_limit(value) {
 
538
+    document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/limit/' + value
 
539
+    document.forms["action"].submit();
 
540
+}
 
541
+function change_page(value) {
 
542
+    document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/page/' + value
 
543
+    document.forms["action"].submit();
 
544
+}
 
545
+</script>
 
546
+
 
547
 <li class="paginate"><label for="limit">Items/page:</label>
 
548
-  <select name="limit" id="limit" onchange="javascript:location='/cobbler_web/{{ what }}/modifylist/limit/'+this.value">
 
549
+  <select name="limit" id="limit" onchange="javascript:change_limit(this.value)">
 
550
   {% for p in pageinfo.items_per_page_list %}
 
551
     <option value="{{ p }}"{% ifequal pageinfo.items_per_page p %} selected="selected"{% endifequal %}>{{ p }}</option>
 
552
   {% endfor %}
 
553
   </select>
 
554
   {% ifnotequal pageinfo.prev_page "~" %}
 
555
-  <a href="/cobbler_web/{{ what }}/modifylist/page/{{ pageinfo.prev_page }}"><span class="lpointers">&lArr;</span></a>
 
556
+  <a href="javascript:change_page('{{ pageinfo.prev_page }}')"><span class="lpointers">&lArr;</span></a>
 
557
   {% else %}
 
558
   <span class="lpointers">&lArr;</span>
 
559
   {% endifnotequal %}
 
560
-  <select name="page" id="page" onchange="javascript:location='/cobbler_web/{{ what }}/modifylist/page/'+this.value">
 
561
+  <select name="page" id="page" onchange="javascript:change_page(this.value)">
 
562
     {% for p in pageinfo.pages %}<option value="{{ p }}"{% ifequal pageinfo.page p %} selected="selected"{% endifequal %}>Page {{ p }}</option>{% endfor %}
 
563
   </select>
 
564
   {% ifnotequal pageinfo.next_page "~" %}
 
565
-  <a href="/cobbler_web/{{ what }}/modifylist/page/{{ pageinfo.next_page }}"><span class="rpointers">&rArr;</span></a>
 
566
+  <a href="javascript:change_page('{{ pageinfo.next_page }}')"><span class="rpointers">&rArr;</span></a>
 
567
   {% else %}
 
568
   <span class="rpointers">&rArr;</span>
 
569
   {% endifnotequal %}