root/trunk/pylucid_project/PyLucid/plugins_internal/filemanager/filemanager.py

Revision 1722, 24.5 kB (checked in by JensDiemer, 4 months ago)

updates for http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges#Movednewformstoforms

  • Property svn:eol-style set to LF
  • Property svn:keywords set to Author Rev LastChangedDate
Line 
1# -*- coding: utf-8 -*-
2
3"""
4    PyLucid media file manager
5    ~~~~~~~~~~~~~~~~~~~~~~~~~~
6
7    We have two kinds of forms: POST actions and GET actions
8    POST actions are:
9        - filemanager.action_mkdir()
10        - filemanager.action_fileupload()
11        - filemanager.action_rmdir()
12        - filemanager.action_deletefile()
13    GET actions are:
14        - filemanager.edit() (Edit a text file)
15
16    POST action note:
17        The POST actions used a hidden field named "action" for easy differ
18        the the current action in the POST data. See below the constants.
19    GET action notes:
20        - The GET forms should not insert a field named "action"!
21        - After a finished GET method, it should display the filelist().
22
23
24    restrictions:
25        - Only tested under linux!
26
27    TODO:
28        - should use posixpath for every URL stuff.
29        - deny editing of binary files (how? ext whitelist or using file?)
30        - insert the basepath selection into the filelist view.
31        - find a way to reduce the redundance.
32        - Write a unitest for the plugin and verify the "bad-char-things" in
33            path/post variables.
34        - Fixe some unicode problems:
35            e.g.: upload a file with a non ascii content
36            use sys.getfilesystemencoding() ?
37        - Check the Plugin under windows - very low priority :)
38
39    Last commit info:
40    ~~~~~~~~~
41    $LastChangedDate$
42    $Rev$
43    $Author$
44
45    :copyleft: 2007-2008 by the PyLucid team, see AUTHORS for more details.
46    :license: GNU GPL v2 or above, see LICENSE for more details
47"""
48
49__version__= "$Rev: $"
50
51import os, cgi, sys, stat, dircache, time
52from datetime import datetime
53
54from django.conf import settings
55from django.http import Http404
56from django import forms
57from django.forms import ValidationError
58from django.utils.translation import ugettext as _
59from django.utils.encoding import force_unicode
60
61from PyLucid.models import Page
62from PyLucid.system.BasePlugin import PyLucidBasePlugin
63
64from PyLucid.tools.path_manager import BASE_PATHS, BASE_PATHS_DICT
65
66#______________________________________________________________________________
67# We use more than one html form in a filelist page. So we need some unique
68# action values for a easier distinguish the POST data.
69ACTION_RMDIR = "0"
70ACTION_MKDIR = "1"
71ACTION_FILEUPLOAD = "2"
72ACTION_DELETEFILE = "3"
73
74#______________________________________________________________________________
75
76def make_dirlist(path, result=[]):
77    """
78    Helper function for building a directory link line.
79    used in filemanager.make_dir_links()
80
81    >>> make_dirlist("/data/one/two")
82    [('data/one/two', 'two'), ('data/one', 'one'), ('data', 'data')]
83    """
84    path = path.strip("/")
85
86    head, tail = os.path.split(path)
87    result.append((path, tail))
88
89    if head:
90        # go recusive deeper
91        return make_dirlist(head, result)
92    else:
93        result.reverse()
94        return result
95
96#______________________________________________________________________________
97
98BAD_DIR_CHARS = ("..", "//", "\\") # Bad characters in directories
99BAD_FILE_CHARS = ("..", "/", "\\") # Bad characters in a filename
100
101def contains_char(text, chars):
102    """
103    returns True if text contains a characters from the given chars list.
104    """
105    for char in chars:
106        if char in text:
107            return True
108    return False
109
110class BadCharField(forms.CharField):
111    """
112    A base class for DirnameField and FilenameField
113    """
114    def __init__(self, max_length=255, min_length=1, required=True,
115                                                            *args, **kwargs):
116        super(BadCharField, self).__init__(
117            max_length, min_length, required, *args, **kwargs
118        )
119
120    def clean(self, value):
121        """
122        Check if a bad caracter is in the form value.
123        """
124        super(BadCharField, self).clean(value)
125        if contains_char(value, self.bad_chars):
126            raise ValidationError(_(u"Error: Bad character found!"))
127
128        if value.startswith("."):
129            raise ValidationError(_(u"Hidden name are not allowed"))
130
131        return value
132
133class DirnameField(BadCharField):
134    """
135    newforms field for verify a dirname.
136    """
137    bad_chars = BAD_DIR_CHARS
138
139class FilenameField(BadCharField):
140    """
141    newforms field for verify a filename.
142    """
143    bad_chars = BAD_FILE_CHARS
144
145#______________________________________________________________________________
146
147class EditFileForm(forms.Form):
148    """ Edit a text file form """
149    filename = FilenameField(
150        help_text=_(
151            u"Change the filename,"
152            " if you want to save the content into a new file."
153        )
154    )
155    content = forms.CharField(
156        widget=forms.Textarea(attrs = {'cols': '80', 'rows': '25'})
157    )
158
159class SelectBasePathForm(forms.Form):
160    """ change the base path form """
161    base_path = forms.ChoiceField(choices=BASE_PATHS)
162
163#______________________________________________________________________________
164
165class ActionField(forms.CharField):
166    """
167    A spezial HiddenInput field for the action string.
168    The action string is set in the forms.Form class and must be the same in
169    the POST data, otherwise the form is not valid.
170    """
171    def __init__(self, action):
172        self.action = action
173        max_length = min_length = len(action)
174        super(ActionField, self).__init__(
175            max_length=max_length, min_length=min_length,
176            required=True, initial=action,
177            widget=forms.HiddenInput,
178        )
179
180    def clean(self, value):
181        super(ActionField, self).clean(value)
182        if value != self.action:
183            raise ValidationError(_(u"Wrong action!"))
184        return value
185
186#______________________________________________________________________________
187
188class CreateDirForm(forms.Form):
189    """
190    Form for creating a new directory
191    ToDo: add a choice field for create a file or a directory action
192    """
193    action = ActionField(ACTION_MKDIR)
194    dirname = DirnameField(
195        help_text="Create a new directory into the current directory."
196    )
197
198
199class UploadFileForm(forms.Form):
200    """ Form to upload a new file """
201    action = ActionField(ACTION_FILEUPLOAD)
202    ufile = forms.FileField(
203        label="filename",
204        help_text="Upload a new file into the current directory."
205    )
206
207class RmDirForm(forms.Form):
208    """
209    Delete a directory.
210    """
211    action = ActionField(ACTION_RMDIR)
212    item_name = DirnameField(
213        help_text="Create a new directory into the current directory."
214    )
215
216class DeleteFileForm(forms.Form):
217    """
218    Delete one file.
219    """
220    action = ActionField(ACTION_DELETEFILE)
221    item_name = FilenameField()
222
223#______________________________________________________________________________
224
225class Path(dict):
226    """
227    Helper class for analyse, check and store the html GET path information.
228    Used in filemanager()
229
230    base_no       = BASE_PATHS_DICT key (Note: it's a String!)
231    base_path     = relative base path
232    abs_base_path = absolute filesystem base path
233    rel_path      = relative path (from GET)
234    abs_path      = absolute filesystem path (abs_base_path + rel_path)
235    url_path      = base_no + rel_path for html links
236
237    - only with new_filename_path():
238    filename      = contains only the filename
239    abs_file_path = absolute filesystem path incl. filename
240    """
241    def __init__(self, context):
242        self.context     = context
243        self.request     = context["request"]
244        self.page_msg    = self.request.page_msg
245
246    def new_dir_path(self, path_info, must_exist=True):
247        """
248        split the html-GET path information and build the absolute filesystem
249        path.
250        if must_exist==True: The given path must allready exists.
251        """
252        if contains_char(path_info, BAD_DIR_CHARS):
253            raise Http404(_(u"Error: Bad character found!"))
254
255        path_info = os.path.normpath(path_info)
256
257        if len(path_info) == 1:
258            # e.g. edit a file in the base_path root
259            base_no = path_info
260            rel_path = ""
261        else:
262            try:
263                base_no, rel_path = path_info.split("/", 1)
264            except ValueError:
265                raise Http404(_("Wrong path!"))
266
267        try:
268            base_path = BASE_PATHS_DICT[base_no]
269        except KeyError:
270            raise Http404(_("Wrong basepath!"))
271
272        base_path = os.path.normpath(base_path)
273        abs_base_path = os.path.abspath(base_path)
274
275        abs_path = os.path.normpath(os.path.join(abs_base_path, rel_path))
276        if must_exist and not os.path.exists(abs_path):
277            raise Http404(_("Error: Path '%s' doesn't exist.") % abs_path)
278
279        self["base_no"] = base_no
280        self["base_path"] = base_path
281        self["abs_base_path"] = abs_base_path
282        self["rel_path"] = rel_path
283        self["abs_path"] = abs_path
284        self["url_path"] = os.path.normpath(os.path.join(base_no, rel_path))
285
286
287    def new_filename_path(self, file_path, must_exist=True):
288        """
289        Split a html GET path information witch contains a filename.
290        if must_exist==True: The file must exist in the given path.
291        """
292        path_info, filename = os.path.split(file_path)
293
294        self.new_dir_path(path_info, must_exist)
295
296        if contains_char(filename, BAD_FILE_CHARS):
297            raise Http404(_(u"Error: Bad character found!"))
298
299        abs_file_path = os.path.join(self["abs_path"], filename)
300
301        if must_exist and not os.path.isfile(abs_file_path):
302            raise Http404(_("Error: File '%s' doesn't exist.") % filename)
303
304        self["filename"] = filename
305        self["abs_file_path"] = abs_file_path
306
307    #--------------------------------------------------------------------------
308
309    def get_abs_link(self, item=""):
310        """
311        returns a absolute link to the given item.
312        """
313        return os.path.join("/", self["base_path"], self["rel_path"], item)
314
315    #--------------------------------------------------------------------------
316
317    def debug(self):
318        """
319        write debug information into the page_msg
320        """
321        self.page_msg("path debug:")
322        for k,v in self.items():
323            self.page_msg(" - %15s: '%s'" % (k,v))
324
325#______________________________________________________________________________
326
327class filemanager(PyLucidBasePlugin):
328    """
329    The PyLucid plugin class.
330    """
331    def __init__(self, context, response):
332        super(filemanager, self).__init__(context, response)
333        self.path = Path(context)
334
335    #--------------------------------------------------------------------------
336
337    def get_filelist(self):
338        """
339        Returns all items in the given directory.
340        -rel_dir is relative to ABS_PATH
341        -the listing is sorted and the first items are the directories.
342        """
343        files = []
344
345        if self.path["rel_path"] == "":
346            # current dir is the media root
347            dirs=[]
348        else:
349            # Add the ".." dir item
350            updir = os.path.split(self.path["rel_path"])[0]
351            dirs = [{
352                "name": "..",
353                "link": self.URLs.methodLink(
354                    method_name="filelist", args=(self.path["base_no"], updir)
355                ),
356                "is_dir": True,
357                "deletable": False,
358                "dont_display_size": True,
359            }]
360
361        link_prefix = self.URLs.methodLink(
362            method_name="filelist", args=self.path["url_path"]
363        )
364
365        for item in sorted(os.listdir(self.path["abs_path"])):
366            if item.startswith("."):
367                # skip hidden files or directories
368                continue
369
370            abs_item_path = os.path.join(self.path["abs_path"], item)
371            statinfo = os.stat(abs_item_path)
372
373            if stat.S_ISDIR(statinfo.st_mode):
374                # Is a directory
375                is_dir = True
376                link = os.path.join(link_prefix, item) + "/"
377                size = len(dircache.listdir(
378                    os.path.join(self.path["abs_path"], item)
379                ))
380            else:
381                is_dir = False
382                link = self.path.get_abs_link(item)
383                size = statinfo.st_size
384
385            mtime = statinfo.st_mtime
386            localtime = time.gmtime(mtime)
387            localdatetime = datetime(*localtime[:6])
388
389            item_dict={
390                "name": item,
391                "link": link,
392                "is_dir": is_dir,
393                "title": abs_item_path,
394                "time": localdatetime,
395                "size": size,
396                "mode": statinfo.st_mode,
397                "uid": statinfo.st_uid,
398                "gid": statinfo.st_gid,
399                "deletable": True,
400            }
401            if is_dir:
402                dirs.append(item_dict)
403            else:
404                files.append(item_dict)
405
406        # return the merged list of direcories and files together
407        dir_list = dirs + files
408        return dir_list
409
410
411    def make_dir_links(self):
412        """
413        Build the context for the path link line.
414        Use the function make_dirlist().
415        """
416        # start with the first base_path entry:
417        dir_links = [{
418            "name": self.path["base_path"], # use only the short relative path
419            "title": self.path["abs_path"],
420            "link": self.URLs.methodLink(
421                method_name="filelist", args=self.path["base_no"]
422            ),
423        }]
424        if self.path["rel_path"] != "":
425            # Not in the root
426            dirlist = make_dirlist(self.path["rel_path"], [])
427            for path, name in dirlist:
428                # append every dir "steps"
429                dir_links.append({
430                    "name": name,
431                    "title": os.path.join(self.path["base_path"], path),
432                    "link": self.URLs.methodLink(
433                        method_name="filelist",
434                        args=(self.path["base_no"], path)
435                    ),
436                })
437
438        return dir_links
439
440    #--------------------------------------------------------------------------
441    # html GET actions:
442
443    def edit(self, path_info):
444        """
445        Edit a text file.
446        """
447        self.path.new_filename_path(path_info, must_exist=True)
448        #self.path.debug()
449
450        try:
451            f = file(self.path["abs_file_path"], "r")
452            content = f.read()
453            f.close()
454            content = content.decode(settings.FILE_CHARSET)
455        except Exception, e:
456            self.page_msg.red("Error, reading file:", e)
457            return
458
459        if self.request.method != 'POST':
460            form = EditFileForm({
461                "content": content,
462                "filename": self.path["filename"],
463            })
464        else: # POST
465            #self.page_msg(self.request.POST)
466            form = EditFileForm(self.request.POST)
467            if form.is_valid():
468                filename = form.cleaned_data["filename"]
469                content = form.cleaned_data["content"]
470                abs_file_path = os.path.join(self.path["abs_path"], filename)
471                try:
472                    content = content.encode(settings.FILE_CHARSET)
473                    f = file(abs_file_path, "w")
474                    f.write(content)
475                    f.close()
476                except Exception, e:
477                    self.page_msg.red("Error, writing file:", e)
478                else:
479                    self.page_msg.green(
480                        "New content saved into '%s'." % filename
481                    )
482                    # Display the filelist
483                    return self.filelist(self.path["url_path"])
484
485        # Don't include the filename in methodLink-args, it always append a
486        # slash!
487        form_link = self.URLs.methodLink(
488            method_name="edit", args=self.path["url_path"]
489        )
490        form_link += self.path["filename"]
491
492        file_path = self.path.get_abs_link()
493
494        # Change the global page title:
495        self.context["PAGE"].title = _("Edit file - %s" % file_path)
496
497        context = {
498            "form_link": form_link,
499            "url_abort": self.URLs.methodLink(
500                method_name="filelist", args=self.path["url_path"]
501            ),
502            "file_path": file_path,
503            "filename": self.path["filename"],
504            "form": form,
505            "charset": settings.FILE_CHARSET,
506        }
507        self._render_template("edit_file", context)#, debug=True)
508
509    #--------------------------------------------------------------------------
510    # html POST actions:
511
512    def action_mkdir(self, dirname):
513        """
514        create a new directory
515        """
516        abs_new_path = os.path.join(self.path["abs_path"], dirname)
517        try:
518            os.mkdir(abs_new_path)
519        except Exception, e:
520            self.page_msg.red("Can't create '%s': %s" % (dirname, e))
521        else:
522            self.page_msg.green("'%s' creaded successfull." % dirname)
523
524
525    def action_fileupload(self, ufile):
526        """
527        save a uploaded file.
528        """
529        filename = ufile.name
530        abs_fs_path = os.path.join(self.path["abs_path"], filename)
531        try:
532            f = file(abs_fs_path,'wb') # if it exists, overwrite
533
534            for chunk in ufile.chunks():
535                f.write(chunk)
536
537            f.close()
538        except Exception, e:
539            self.page_msg.red("Can't write file: '%s'" % e)
540            return
541
542        statinfo = os.stat(abs_fs_path)
543        real_filesize = statinfo.st_size
544
545        if real_filesize == ufile.size:
546            self.page_msg.green(
547                "File '%s' written successfull. (%s Bytes)" % (
548                    filename, real_filesize
549                )
550            )
551        else: # Should never appear
552            self.page_msg.red(
553                "Error writing file '%s'."
554                " Filesize is different:"
555                " Should be %s Bytes, but is %s Bytes" % (
556                    ufile.file_size, real_filesize
557                )
558            )
559
560
561    def action_rmdir(self, dirname):
562        """ delete a directory """
563        abs_fs_path = os.path.join(self.path["abs_path"], dirname)
564        try:
565            os.rmdir(abs_fs_path)
566        except Exception, e:
567            self.page_msg.red("Can't delete '%s': %s" % (dirname, e))
568        else:
569            self.page_msg.green("'%s' deleted successfull." % dirname)
570
571
572    def action_deletefile(self, filename):
573        """ delete a file """
574        abs_fs_path = os.path.join(self.path["abs_path"], filename)
575        try:
576            os.remove(abs_fs_path)
577        except Exception, e:
578            self.page_msg.red("Can't delete '%s': %s" % (filename, e))
579        else:
580            self.page_msg.green("File '%s' deleted successfull." % filename)
581
582    #--------------------------------------------------------------------------
583
584    def userinfo(self, old_path=""):
585        """
586        Display some user information related to the filemanager functionality.
587        """
588        # Change the global page title:
589        self.context["PAGE"].title = _(
590            "Filemanager - Display some user information"
591        )
592
593        import pwd, grp
594
595        uid = os.getuid()
596        gid = os.getgid()
597
598        pwd_info = pwd.getpwuid(os.getuid())
599        grp_info = grp.getgrgid(os.getgid())
600
601        context = {
602            "filelist_link": self.URLs.methodLink(
603                method_name="filelist", args=old_path
604            ),
605            "uid": uid,
606            "gid": gid,
607            "pwd_info": pwd_info,
608            "grp_info": grp_info,
609        }
610#        self.page_msg(context)
611        self._render_template("userinfo", context)#, debug=True)
612
613    def select_basepath(self):
614        """
615        change the basepath, after send the form, we display the filelist
616        """
617        # Change the global page title:
618        self.context["PAGE"].title = _("Filemanager - Change the basepath")
619#        self.page_msg(self.request.POST)
620
621        if self.request.method == 'POST':
622            form = SelectBasePathForm(self.request.POST)
623            if form.is_valid():
624                path_no = form.cleaned_data["base_path"]
625                new_path = BASE_PATHS_DICT[path_no]
626                if not os.path.isdir(new_path):
627                    self.page_msg.red(
628                        "Error: Path '%s' doesn't exist" % new_path
629                    )
630                else:
631                    self.page_msg("change base path to '%s'." % new_path)
632                    # Display the filelist:
633                    return self.filelist(path_no + "/")
634        else:
635            form = SelectBasePathForm()
636
637        self._render_template("select_basepath", {"form": form})#, debug=True)
638
639    #--------------------------------------------------------------------------
640
641    def filelist(self, path_info=None):
642        """
643        List dir and file. Some actions.
644        rest: path to dir or file
645        """
646        if not path_info:
647            self.select_basepath()
648            return
649
650        # analyse and store the given GET path infomation
651        self.path.new_dir_path(path_info, must_exist=True)
652        #self.path.debug()
653
654        # Change the global page title:
655        path = os.path.join(self.path["base_path"], self.path["rel_path"])
656        self.context["PAGE"].title = _("File list - %s" % path)
657
658        # We init all forms before we check the POST, because we only need
659        # error information for the current action. Only the form for the
660        # current action should be inited with self.request.POST!
661        ufile_form = UploadFileForm()
662        mkdir_form = CreateDirForm()
663
664        if self.request.method == 'POST' and "action" in self.request.POST:
665            #self.page_msg(self.request.POST)
666            action = self.request.POST["action"]
667
668            #------------------------------------------------------------------
669            # action in the current directory:
670
671            if action == ACTION_MKDIR:
672                # Create a new directory
673                mkdir_form = CreateDirForm(self.request.POST)
674                if mkdir_form.is_valid():
675                    dirname = mkdir_form.cleaned_data["dirname"]
676                    self.action_mkdir(dirname)
677
678            elif action == ACTION_FILEUPLOAD:
679                # save a uploaded file
680                ufile_form = UploadFileForm(
681                    self.request.POST, self.request.FILES
682                )
683                if ufile_form.is_valid():
684                    ufile = ufile_form.cleaned_data["ufile"]
685                    self.action_fileupload(ufile)
686
687            #------------------------------------------------------------------
688            # action for one file/directory:
689
690            elif action == ACTION_RMDIR:
691                # delete a directory
692                form = RmDirForm(self.request.POST)
693                if form.is_valid():
694                    dirname = form.cleaned_data["item_name"]
695                    self.action_rmdir(dirname)
696
697            elif action == ACTION_DELETEFILE:
698                # delete a file
699                form = DeleteFileForm(self.request.POST)
700                if form.is_valid():
701                    filename = form.cleaned_data["item_name"]
702                    self.action_deletefile(filename)
703
704        # build the directory+file list:
705        dir_list = self.get_filelist()
706
707        # Build the path link line:
708        dir_links = self.make_dir_links()
709
710        # if the current path is writeable?
711        writeable = os.access(self.path["abs_path"], os.W_OK)
712
713        context = {
714#            "path": self.path,
715            "post_url": self.URLs.methodLink(
716                method_name="filelist", args=self.path["url_path"]
717            ),
718
719            "mkdir_form": mkdir_form,
720            "ufile_form": ufile_form,
721
722            "ACTION_RMDIR": ACTION_RMDIR,
723            "ACTION_DELETEFILE": ACTION_DELETEFILE,
724
725            "dir_links": dir_links,
726            "writeable": writeable,
727            "dir_list": dir_list,
728
729            "userinfo_link": self.URLs.methodLink(
730                method_name="userinfo", args=self.path["url_path"]
731            ),
732            "edit_link": self.URLs.methodLink(
733                method_name="edit", args=self.path["url_path"]
734            ),
735            "change_basepath_link": self.URLs.methodLink(
736                method_name="select_basepath"
737            ),
738        }
739#        self.page_msg(context)
740#        self._render_template("filelist", context, debug=True)
741        self._render_template("filelist", context)#, debug=True)
742
743# -----------------------------------------------------------------------------
744
745class WrongDirectory(Exception):
746    def __init__(self, value):
747        self.value = value
748    def __str__(self):
749        return repr(self.value)
750
751class BadFilename(Exception):
752    """ A not allowed character contain a filename """
753    pass
754
755class BadPath(Exception):
756    """ A not allowed character contain a path """
757    pass