Procházet zdrojové kódy

Merge pull request #545 from inkhey/webdav_move_and_rename_fix

Damien Accorsi před 6 roky
rodič
revize
4ab43571a1
No account linked to committer's email

+ 4 - 0
doc/apache.md Zobrazit soubor

@@ -109,6 +109,10 @@ Add `webdav` at `root_path` in the `[tracim_path]/tracim/wsgidav.conf`:
109 109
     
110 110
     root_path = ''
111 111
     
112
+    # Tracim doesn't support digest auth for webdav
113
+    acceptbasic = True
114
+    acceptdigest = False
115
+    defaultdigest = False
112 116
     #===============================================================================
113 117
     # Lock Manager
114 118
     #

+ 70 - 0
doc/webdav.md Zobrazit soubor

@@ -0,0 +1,70 @@
1
+# How to use webdav from different OS ?
2
+
3
+## Windows
4
+
5
+### Windows 7
6
+
7
+- Open Start Menu.
8
+- Click on Computer.
9
+- click on "Map network drive".
10
+
11
+### Windows 8 and 10
12
+
13
+- Open File explorer.
14
+- Right click on "This PC" (left panel)
15
+- From the dropdown menu, select "Map network drive".
16
+
17
+### Map Network drive Windows:
18
+
19
+Webdav Windows addresses are similar to:
20
+
21
+```
22
+https://<yourinstance>/webdav/ (secure)
23
+http://<yourinstance>/webdav/ (unsecure)
24
+```
25
+
26
+- Enter the address of webdav (you can find it in each workspace, under workspace details)
27
+- Check "Reconnect at sign-in" and "Connect using different credentials".
28
+- Click "Finish".
29
+- Your login/password will be ask. Use your Tracim credentials.
30
+- After that, your webdav access should be mounted.
31
+
32
+### Unsecure HTTP using Windows
33
+
34
+If you want to use webdav with tracim without https, you need to set Windows to accept basic auth in http.
35
+
36
+To enable it:
37
+- Launch regedit.
38
+- Go to "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters\BasicAuthLevel".
39
+- set "BasicAuthLevel" to "2".
40
+
41
+## OSX
42
+
43
+Webdav OSX addresses are similar to:
44
+
45
+```
46
+https://<yourinstance>/webdav/ (secure)
47
+http://<yourinstance>/webdav/ (unsecure)
48
+```
49
+
50
+- In the Finder, choose "Go > Connect to Server".
51
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Click Connect.
52
+- Your login/password will be ask. Use your Tracim credentials.
53
+- After that, your webdav access should be mounted.
54
+
55
+## Linux
56
+
57
+Webdav Linux addresses are similar to:
58
+
59
+```
60
+davs://<yourinstance>/webdav/ (secure)
61
+dav://<yourinstance>/webdav/ (unsecure)
62
+```
63
+
64
+### Gnome3 (nautilus)
65
+
66
+- Launch nautilus.
67
+- Show url bar : Ctrl+l.
68
+- Enter the address of webdav (you can find it in each workspace, under workspace details). Press Enter.
69
+- Your login/password will be ask. Use your Tracim credentials.
70
+- After that, your webdav access should be mounted.

+ 26 - 0
tracim/migration/versions/2b4043fa2502_remove_webdav_right_digest_response_.py Zobrazit soubor

@@ -0,0 +1,26 @@
1
+"""remove webdav_right_digest_response_hash from database
2
+
3
+Revision ID: 2b4043fa2502
4
+Revises: f3852e1349c4
5
+Create Date: 2018-03-13 14:41:38.590375
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '2b4043fa2502'
11
+down_revision = 'f3852e1349c4'
12
+
13
+from alembic import op
14
+from sqlalchemy import Column, Unicode
15
+
16
+
17
+def upgrade():
18
+    with op.batch_alter_table('users') as batch_op:
19
+        batch_op.drop_column('webdav_left_digest_response_hash')
20
+
21
+
22
+def downgrade():
23
+    with op.batch_alter_table('users') as batch_op:
24
+        batch_op.add_column(
25
+            Column('webdav_left_digest_response_hash', Unicode(128))
26
+        )

+ 17 - 0
tracim/tracim/lib/content.py Zobrazit soubor

@@ -29,6 +29,7 @@ from sqlalchemy.sql.elements import and_
29 29
 from tracim.lib import cmp_to_key
30 30
 from tracim.lib.notifications import NotifierFactory
31 31
 from tracim.lib.utils import SameValueError
32
+from tracim.lib.utils import current_date_for_filename
32 33
 from tracim.model import DBSession
33 34
 from tracim.model import new_revision
34 35
 from tracim.model.auth import User
@@ -894,6 +895,14 @@ class ContentApi(object):
894 895
     def archive(self, content: Content):
895 896
         content.owner = self._user
896 897
         content.is_archived = True
898
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
899
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
900
+        # un-archiving file.
901
+        content.label = '{label}-{action}-{date}'.format(
902
+            label=content.label,
903
+            action='archived',
904
+            date=current_date_for_filename()
905
+        )
897 906
         content.revision_type = ActionDescription.ARCHIVING
898 907
 
899 908
     def unarchive(self, content: Content):
@@ -904,6 +913,14 @@ class ContentApi(object):
904 913
     def delete(self, content: Content):
905 914
         content.owner = self._user
906 915
         content.is_deleted = True
916
+        # TODO - G.M - 12-03-2018 - Inspect possible label conflict problem
917
+        # INFO - G.M - 12-03-2018 - Set label name to avoid trouble when
918
+        # un-deleting file.
919
+        content.label = '{label}-{action}-{date}'.format(
920
+            label=content.label,
921
+            action='deleted',
922
+            date=current_date_for_filename()
923
+        )
907 924
         content.revision_type = ActionDescription.DELETION
908 925
 
909 926
     def undelete(self, content: Content):

+ 2 - 2
tracim/tracim/lib/daemons.py Zobrazit soubor

@@ -390,14 +390,14 @@ class WsgiDavDaemon(Daemon):
390 390
             print(
391 391
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
392 392
         from wsgidav.dir_browser import WsgiDavDirBrowser
393
-        from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
393
+        from wsgidav.http_authenticator import HTTPAuthenticator
394 394
         from wsgidav.error_printer import ErrorPrinter
395 395
         from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
396 396
 
397 397
         config['middleware_stack'] = [
398 398
             TracimEnforceHTTPS,
399 399
             WsgiDavDirBrowser,
400
-            TracimHTTPAuthenticator,
400
+            HTTPAuthenticator,
401 401
             ErrorPrinter,
402 402
             TracimWsgiDavDebugFilter,
403 403
         ]

+ 11 - 0
tracim/tracim/lib/utils.py Zobrazit soubor

@@ -1,4 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
+import datetime
2 3
 import os
3 4
 import time
4 5
 import signal
@@ -182,6 +183,16 @@ def get_rq_queue(queue_name: str= 'default') -> Queue:
182 183
         db=cfg.EMAIL_SENDER_REDIS_DB,
183 184
     ))
184 185
 
186
+def current_date_for_filename() -> str:
187
+    """
188
+    ISO8601 current date, adapted to be used in filename (for
189
+    webdav feature for example), with trouble-free characters.
190
+    :return: current date as string like "2018-03-19T15.49.27.246592"
191
+    """
192
+    # INFO - G.M - 19-03-2018 - As ':' is in transform_to_bdd method in
193
+    # webdav utils, it may cause trouble. So, it should be replaced to
194
+    # a character which will not change in bdd.
195
+    return datetime.datetime.now().isoformat().replace(':', '.')
185 196
 
186 197
 class TracimEnforceHTTPS(BaseMiddleware):
187 198
 

+ 2 - 2
tracim/tracim/lib/webdav/design.py Zobrazit soubor

@@ -148,7 +148,7 @@ def create_readable_date(created, delta_from_datetime: datetime = None):
148 148
     return aff
149 149
 
150 150
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
151
-    hist = content.get_history()
151
+    hist = content.get_history(drop_empty_revision=False)
152 152
     histHTML = '<table class="table table-striped table-hover">'
153 153
     for event in hist:
154 154
         if isinstance(event, VirtualEvent):
@@ -237,7 +237,7 @@ def designPage(content: data.Content, content_revision: data.ContentRevisionRO)
237 237
     return page
238 238
 
239 239
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
240
-        hist = content.get_history()
240
+        hist = content.get_history(drop_empty_revision=False)
241 241
 
242 242
         allT = []
243 243
         allT += comments

+ 11 - 11
tracim/tracim/lib/webdav/sql_domain_controller.py Zobrazit soubor

@@ -2,6 +2,9 @@
2 2
 
3 3
 from tracim.lib.user import UserApi
4 4
 
5
+class DigestAuthNotImplemented(Exception):
6
+    pass
7
+
5 8
 class TracimDomainController(object):
6 9
     """
7 10
     The domain controller is used by http_authenticator to authenticate the user every time a request is
@@ -13,6 +16,14 @@ class TracimDomainController(object):
13 16
     def getDomainRealm(self, inputURL, environ):
14 17
         return '/'
15 18
 
19
+    def getRealmUserPassword(self, realmname, username, environ):
20
+        """
21
+        This method is normally only use for digest auth. wsgidav need
22
+        plain password to deal with it. as we didn't
23
+        provide support for this kind of auth, this method raise an exception.
24
+        """
25
+        raise DigestAuthNotImplemented
26
+
16 27
     def requireAuthentication(self, realmname, environ):
17 28
         return True
18 29
 
@@ -27,17 +38,6 @@ class TracimDomainController(object):
27 38
         except:
28 39
             return False
29 40
 
30
-    def get_left_digest_response_hash(self, realmname, username, environ):
31
-        """
32
-        Called by our http_authenticator to get the hashed md5 digest for the current user that is also sent by
33
-        the webdav client
34
-        """
35
-        try:
36
-            user = self._api.get_one_by_email(username)
37
-            return user.webdav_left_digest_response_hash
38
-        except:
39
-            return None
40
-
41 41
     def authDomainUser(self, realmname, username, password, environ):
42 42
         """
43 43
         If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by

+ 35 - 62
tracim/tracim/lib/webdav/sql_resources.py Zobrazit soubor

@@ -36,7 +36,8 @@ logger = logging.getLogger()
36 36
 
37 37
 class ManageActions(object):
38 38
     """
39
-    This object is used to encapsulate all Deletion/Archiving related method as to not duplicate too much code
39
+    This object is used to encapsulate all Deletion/Archiving related
40
+    method as to not duplicate too much code
40 41
     """
41 42
     def __init__(self, action_type: str, api: ContentApi, content: Content):
42 43
         self.content_api = api
@@ -49,57 +50,14 @@ class ManageActions(object):
49 50
             ActionDescription.UNDELETION: self.content_api.undelete
50 51
         }
51 52
 
52
-        self._to_name = {
53
-            ActionDescription.ARCHIVING: 'archived',
54
-            ActionDescription.DELETION: 'deleted'
55
-        }
56
-
57 53
         self._type = action_type
58
-        self._new_name = self.make_name()
59 54
 
60 55
     def action(self):
61
-        try:
62
-            # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
63
-            # don't get an error and the database request send back a result, we stop the action
64
-            self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent)
65
-            raise DAVError(HTTP_FORBIDDEN)
66
-        except NoResultFound:
67
-            with new_revision(self.content):
68
-                self.content_api.update_content(self.content, self._new_name)
69
-                self._actions[self._type](self.content)
70
-                self.content_api.save(self.content, self._type)
71
-
72
-            transaction.commit()
73
-
74
-    def make_name(self) -> str:
75
-        """
76
-        Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
77
-        removing this string when undeleting/unarchiving
78
-        """
79
-        new_name = self.content.get_label_as_file()
80
-        extension = ''
81
-
82
-        # if the content has no label, the last .ext is important
83
-        # thus we want to rename a file from 'file.txt' to 'file - deleted... .txt' and not 'file.txt - deleted...'
84
-        is_file_name = self.content.label == ''
85
-        if is_file_name:
86
-            search = re.search(r'(\.[^.]+)$', new_name)
87
-            if search:
88
-                extension = search.group(0)
89
-            new_name = re.sub(r'(\.[^.]+)$', '', new_name)
90
-
91
-        if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]:
92
-            new_name += ' - %s the %s' % (self._to_name[self._type], datetime.now().strftime('%d-%m-%Y at %H:%M'))
93
-        else:
94
-            new_name = re.sub(
95
-                r'( - (%s|%s) the .*)$' % (self._to_name[ActionDescription.DELETION], self._to_name[ActionDescription.ARCHIVING]),
96
-                '',
97
-                new_name
98
-            )
99
-
100
-        new_name += extension
56
+        with new_revision(self.content):
57
+            self._actions[self._type](self.content)
58
+            self.content_api.save(self.content, self._type)
101 59
 
102
-        return new_name
60
+        transaction.commit()
103 61
 
104 62
 
105 63
 class Root(DAVCollection):
@@ -964,12 +922,25 @@ class File(DAVNonCollection):
964 922
         if invalid_path:
965 923
             raise DAVError(HTTP_FORBIDDEN)
966 924
 
967
-    def move_file(self, destpath):
925
+    def move_file(self, destpath: str) -> None:
926
+        """
927
+        Move file mean changing the path to access to a file. This can mean
928
+        simple renaming(1), moving file from a directory to one another(2)
929
+        but also renaming + moving file from a directory to one another at
930
+        the same time (3).
931
+
932
+        (1): move /dir1/file1 -> /dir1/file2
933
+        (2): move /dir1/file1 -> /dir2/file1
934
+        (3): move /dir1/file1 -> /dir2/file2
935
+        :param destpath: destination path of webdav move
936
+        :return: nothing
937
+        """
968 938
 
969 939
         workspace = self.content.workspace
970 940
         parent = self.content.parent
971 941
 
972 942
         with new_revision(self.content):
943
+            # INFO - G.M - 2018-03-09 - First, renaming file if needed
973 944
             if basename(destpath) != self.getDisplayName():
974 945
                 new_given_file_name = transform_to_bdd(basename(destpath))
975 946
                 new_file_name, new_file_extension = \
@@ -981,21 +952,23 @@ class File(DAVNonCollection):
981 952
                 )
982 953
                 self.content.file_extension = new_file_extension
983 954
                 self.content_api.save(self.content)
984
-            else:
985
-                workspace_api = WorkspaceApi(self.user)
986
-                content_api = ContentApi(self.user)
987 955
 
988
-                destination_workspace = self.provider.get_workspace_from_path(
989
-                    destpath,
990
-                    workspace_api,
991
-                )
992
-
993
-                destination_parent = self.provider.get_parent_from_path(
994
-                    destpath,
995
-                    content_api,
996
-                    destination_workspace,
997
-                )
956
+            # INFO - G.M - 2018-03-09 - Moving file if needed
957
+            workspace_api = WorkspaceApi(self.user)
958
+            content_api = ContentApi(self.user)
998 959
 
960
+            destination_workspace = self.provider.get_workspace_from_path(
961
+                destpath,
962
+                workspace_api,
963
+            )
964
+            destination_parent = self.provider.get_parent_from_path(
965
+                destpath,
966
+                content_api,
967
+                destination_workspace,
968
+            )
969
+            if destination_parent != parent or destination_workspace != workspace:  # nopep8
970
+                #  INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place"  # nopep8
971
+                #  if the request does not result in a real move.
999 972
                 self.content_api.move(
1000 973
                     item=self.content,
1001 974
                     new_parent=destination_parent,

+ 0 - 156
tracim/tracim/lib/webdav/tracim_http_authenticator.py Zobrazit soubor

@@ -1,156 +0,0 @@
1
-import os
2
-import re
3
-
4
-from wsgidav.http_authenticator import HTTPAuthenticator
5
-from wsgidav import util
6
-import cherrypy
7
-
8
-from tracim.lib.user import CurrentUserGetterApi
9
-from tracim.lib.user import CURRENT_USER_WSGIDAV
10
-
11
-_logger = util.getModuleLogger(__name__, True)
12
-HOTFIX_WINXP_AcceptRootShareLogin = True
13
-
14
-
15
-class TracimHTTPAuthenticator(HTTPAuthenticator):
16
-    def __init__(self, application, config):
17
-        super(TracimHTTPAuthenticator, self).__init__(application, config)
18
-        self._headerfixparser = re.compile(r'([\w]+)=("[^"]*,[^"]*"),')
19
-
20
-    def authDigestAuthRequest(self, environ, start_response):
21
-        realmname = self._domaincontroller.getDomainRealm(environ["PATH_INFO"], environ)
22
-
23
-        isinvalidreq = False
24
-
25
-        authheaderdict = dict([])
26
-        authheaders = environ["HTTP_AUTHORIZATION"] + ","
27
-        if not authheaders.lower().strip().startswith("digest"):
28
-            isinvalidreq = True
29
-            # Hotfix for Windows file manager and OSX Finder:
30
-        # Some clients don't urlencode paths in auth header, so uri value may
31
-        # contain commas, which break the usual regex headerparser. Example:
32
-        # Digest username="user",realm="/",uri="a,b.txt",nc=00000001, ...
33
-        # -> [..., ('uri', '"a'), ('nc', '00000001'), ...]
34
-        # Override any such values with carefully extracted ones.
35
-        authheaderlist = self._headerparser.findall(authheaders)
36
-        authheaderfixlist = self._headerfixparser.findall(authheaders)
37
-        if authheaderfixlist:
38
-            _logger.info("Fixing authheader comma-parsing: extend %s with %s" \
39
-                         % (authheaderlist, authheaderfixlist))
40
-            authheaderlist += authheaderfixlist
41
-        for authheader in authheaderlist:
42
-            authheaderkey = authheader[0]
43
-            authheadervalue = authheader[1].strip().strip("\"")
44
-            authheaderdict[authheaderkey] = authheadervalue
45
-
46
-        _logger.debug("authDigestAuthRequest: %s" % environ["HTTP_AUTHORIZATION"])
47
-        _logger.debug("  -> %s" % authheaderdict)
48
-
49
-        if "username" in authheaderdict:
50
-            req_username = authheaderdict["username"]
51
-            req_username_org = req_username
52
-            # Hotfix for Windows XP:
53
-            #   net use W: http://127.0.0.1/dav /USER:DOMAIN\tester tester
54
-            # will send the name with double backslashes ('DOMAIN\\tester')
55
-            # but send the digest for the simple name ('DOMAIN\tester').
56
-            if r"\\" in req_username:
57
-                req_username = req_username.replace("\\\\", "\\")
58
-                _logger.info("Fixing Windows name with double backslash: '%s' --> '%s'" % (req_username_org, req_username))
59
-
60
-            if not self._domaincontroller.isRealmUser(realmname, req_username, environ):
61
-                isinvalidreq = True
62
-        else:
63
-            isinvalidreq = True
64
-
65
-            # TODO: Chun added this comments, but code was commented out
66
-            # Do not do realm checking - a hotfix for WinXP using some other realm's
67
-            # auth details for this realm - if user/password match
68
-
69
-        if 'realm' in authheaderdict:
70
-            if authheaderdict["realm"].upper() != realmname.upper():
71
-                if HOTFIX_WINXP_AcceptRootShareLogin:
72
-                    # Hotfix: also accept '/'
73
-                    if authheaderdict["realm"].upper() != "/":
74
-                        isinvalidreq = True
75
-                else:
76
-                    isinvalidreq = True
77
-
78
-        if "algorithm" in authheaderdict:
79
-            if authheaderdict["algorithm"].upper() != "MD5":
80
-                isinvalidreq = True  # only MD5 supported
81
-
82
-        if "uri" in authheaderdict:
83
-            req_uri = authheaderdict["uri"]
84
-
85
-        if "nonce" in authheaderdict:
86
-            req_nonce = authheaderdict["nonce"]
87
-        else:
88
-            isinvalidreq = True
89
-
90
-        req_hasqop = False
91
-        if "qop" in authheaderdict:
92
-            req_hasqop = True
93
-            req_qop = authheaderdict["qop"]
94
-            if req_qop.lower() != "auth":
95
-                isinvalidreq = True  # only auth supported, auth-int not supported
96
-        else:
97
-            req_qop = None
98
-
99
-        if "cnonce" in authheaderdict:
100
-            req_cnonce = authheaderdict["cnonce"]
101
-        else:
102
-            req_cnonce = None
103
-            if req_hasqop:
104
-                isinvalidreq = True
105
-
106
-        if "nc" in authheaderdict:  # is read but nonce-count checking not implemented
107
-            req_nc = authheaderdict["nc"]
108
-        else:
109
-            req_nc = None
110
-            if req_hasqop:
111
-                isinvalidreq = True
112
-
113
-        if "response" in authheaderdict:
114
-            req_response = authheaderdict["response"]
115
-        else:
116
-            isinvalidreq = True
117
-
118
-        if not isinvalidreq:
119
-            left_digest_response_hash = self._domaincontroller.get_left_digest_response_hash(realmname, req_username, environ)
120
-
121
-            req_method = environ["REQUEST_METHOD"]
122
-
123
-            required_digest = self.tracim_compute_digest_response(left_digest_response_hash, req_method, req_uri, req_nonce,
124
-                                                         req_cnonce, req_qop, req_nc)
125
-
126
-            if required_digest != req_response:
127
-                _logger.warning("computeDigestResponse('%s', '%s', ...): %s != %s" % (
128
-                realmname, req_username, required_digest, req_response))
129
-                isinvalidreq = True
130
-            else:
131
-                _logger.debug("digest succeeded for realm '%s', user '%s'" % (realmname, req_username))
132
-            pass
133
-
134
-        if isinvalidreq:
135
-            _logger.warning("Authentication failed for user '%s', realm '%s'" % (req_username, realmname))
136
-            return self.sendDigestAuthResponse(environ, start_response)
137
-
138
-        environ["http_authenticator.realm"] = realmname
139
-        environ["http_authenticator.username"] = req_username
140
-
141
-        # Set request current user email to be able to recognise him later
142
-        cherrypy.request.current_user_email = req_username
143
-        CurrentUserGetterApi.set_thread_local_getter(CURRENT_USER_WSGIDAV)
144
-
145
-        return self._application(environ, start_response)
146
-
147
-    def tracim_compute_digest_response(self, left_digest_response_hash, method, uri, nonce, cnonce, qop, nc):
148
-        # A1 : username:realm:password
149
-        A2 = method + ":" + uri
150
-        if qop:
151
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + self.md5h(A2))
152
-        else:
153
-            digestresp = self.md5kd(left_digest_response_hash, nonce + ":" + self.md5h(A2))
154
-            # print(A1, A2)
155
-        # print(digestresp)
156
-        return digestresp

+ 0 - 25
tracim/tracim/model/auth.py Zobrazit soubor

@@ -128,7 +128,6 @@ class User(DeclarativeBase):
128 128
     is_active = Column(Boolean, default=True, nullable=False)
129 129
     imported_from = Column(Unicode(32), nullable=True)
130 130
     timezone = Column(Unicode(255), nullable=False, server_default='')
131
-    _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
132 131
     auth_token = Column(Unicode(255))
133 132
     auth_token_created = Column(DateTime)
134 133
 
@@ -202,10 +201,8 @@ class User(DeclarativeBase):
202 201
 
203 202
         Hash cleartext password on the fly,
204 203
         Store its ciphertext version,
205
-        Update the WebDAV hash as well.
206 204
         """
207 205
         self._password = self._hash_password(cleartext_password)
208
-        self.update_webdav_digest_auth(cleartext_password)
209 206
 
210 207
     def _get_password(self) -> str:
211 208
         """Return the hashed version of the password."""
@@ -214,26 +211,6 @@ class User(DeclarativeBase):
214 211
     password = synonym('_password', descriptor=property(_get_password,
215 212
                                                         _set_password))
216 213
 
217
-    @classmethod
218
-    def _hash_digest(cls, digest):
219
-        return md5(bytes(digest, 'utf-8')).hexdigest()
220
-
221
-    def _set_hash_digest(self, digest):
222
-        self._webdav_left_digest_response_hash = self._hash_digest(digest)
223
-
224
-    def _get_hash_digest(self):
225
-        return self._webdav_left_digest_response_hash
226
-
227
-    webdav_left_digest_response_hash = synonym('_webdav_left_digest_response_hash',
228
-                                               descriptor=property(_get_hash_digest,
229
-                                                                   _set_hash_digest))
230
-
231
-    def update_webdav_digest_auth(self, cleartext_password: str) -> None:
232
-        self.webdav_left_digest_response_hash \
233
-            = '{username}:/:{cleartext_password}'.format(
234
-                username=self.email,
235
-                cleartext_password=cleartext_password,
236
-            )
237 214
 
238 215
     def validate_password(self, cleartext_password: str) -> bool:
239 216
         """
@@ -252,8 +229,6 @@ class User(DeclarativeBase):
252 229
             hash = sha256()
253 230
             hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
254 231
             result = self.password[64:] == hash.hexdigest()
255
-            if result and not self.webdav_left_digest_response_hash:
256
-                self.update_webdav_digest_auth(cleartext_password)
257 232
         return result
258 233
 
259 234
     def get_display_name(self, remove_email_part: bool=False) -> str:

+ 15 - 2
tracim/tracim/model/data.py Zobrazit soubor

@@ -1234,11 +1234,24 @@ class Content(DeclarativeBase):
1234 1234
 
1235 1235
         return ContentType.sorted(types)
1236 1236
 
1237
-    def get_history(self) -> '[VirtualEvent]':
1237
+    def get_history(self, drop_empty_revision=False) -> '[VirtualEvent]':
1238 1238
         events = []
1239 1239
         for comment in self.get_comments():
1240 1240
             events.append(VirtualEvent.create_from_content(comment))
1241
-        for revision in self.revisions:
1241
+
1242
+        revisions = sorted(self.revisions, key=lambda rev: rev.revision_id)
1243
+        for revision in revisions:
1244
+            # INFO - G.M - 09-03-2018 - Do not show file revision with empty
1245
+            # file to have a more clear view of revision.
1246
+            # Some webdav client create empty file before uploading, we must
1247
+            # have possibility to not show the related revision
1248
+            if drop_empty_revision:
1249
+                if revision.depot_file and revision.depot_file.file.content_length == 0:  # nopep8
1250
+                    # INFO - G.M - 12-03-2018 -Always show the last and
1251
+                    # first revision.
1252
+                    if revision != revisions[-1] and revision != revisions[0]:
1253
+                        continue
1254
+
1242 1255
             events.append(VirtualEvent.create_from_content_revision(revision))
1243 1256
 
1244 1257
         sorted_events = sorted(events,

+ 2 - 2
tracim/tracim/model/serializers.py Zobrazit soubor

@@ -398,7 +398,7 @@ def serialize_node_for_page(content: Content, context: Context):
398 398
             links=[],
399 399
             revision_nb = len(content.revisions),
400 400
             selected_revision='latest' if content.revision_to_serialize<=0 else content.revision_to_serialize,
401
-            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history()),
401
+            history=Context(CTX.CONTENT_HISTORY).toDict(content.get_history(drop_empty_revision=True)),  # nopep8
402 402
             is_editable=content.is_editable,
403 403
             is_deleted=content.is_deleted,
404 404
             is_archived=content.is_archived,
@@ -475,7 +475,7 @@ def serialize_node_for_thread(item: Content, context: Context):
475 475
             workspace = context.toDict(item.workspace),
476 476
             comments = reversed(context.toDict(item.get_comments())),
477 477
             is_new=item.has_new_information_for(context.get_user()),
478
-            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history()),
478
+            history = Context(CTX.CONTENT_HISTORY).toDict(item.get_history(drop_empty_revision=True)),  # nopep8
479 479
             is_editable=item.is_editable,
480 480
             is_deleted=item.is_deleted,
481 481
             is_archived=item.is_archived,

+ 0 - 6
tracim/tracim/tests/command/user.py Zobrazit soubor

@@ -14,7 +14,6 @@ class TestUserCommand(TestCommand):
14 14
         # Check webdav digest exist for this user
15 15
         user = DBSession.query(User)\
16 16
             .filter(User.email == 'new-user@algoo.fr').one()
17
-        ok_(user.webdav_left_digest_response_hash)
18 17
 
19 18
     def test_update_password(self):
20 19
         self._create_user('new-user@algoo.fr', 'toor')
@@ -22,7 +21,6 @@ class TestUserCommand(TestCommand):
22 21
         # Grab webdav digest
23 22
         user = DBSession.query(User) \
24 23
             .filter(User.email == 'new-user@algoo.fr').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26 24
 
27 25
         self._execute_command(
28 26
             UpdateUserCommand,
@@ -35,10 +33,6 @@ class TestUserCommand(TestCommand):
35 33
         # Grab new webdav digest to compare it
36 34
         user = DBSession.query(User) \
37 35
             .filter(User.email == 'new-user@algoo.fr').one()
38
-        ok_(
39
-            webdav_digest != user.webdav_left_digest_response_hash,
40
-            msg='Webdav digest should be different',
41
-        )
42 36
 
43 37
     def test_create_with_group(self):
44 38
         more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']

+ 0 - 8
tracim/tracim/tests/functional/test_admin.py Zobrazit soubor

@@ -43,9 +43,6 @@ class TestAuthentication(TracimTestController):
43 43
         ok_(user, msg="User should exist now")
44 44
         ok_(user.validate_password('password'))
45 45
 
46
-        # User must have webdav digest
47
-        ok_(user.webdav_left_digest_response_hash)
48
-
49 46
     def test_update_user_password(self):
50 47
         self._connect_user(
51 48
             'admin@admin.admin',
@@ -67,7 +64,6 @@ class TestAuthentication(TracimTestController):
67 64
 
68 65
         user = DBSession.query(User) \
69 66
             .filter(User.email == 'an-other-email@test.local').one()
70
-        webdav_digest = user.webdav_left_digest_response_hash
71 67
 
72 68
         self.app.post(
73 69
             '/admin/users/{user_id}/password?_method=PUT'.format(
@@ -82,7 +78,3 @@ class TestAuthentication(TracimTestController):
82 78
         user = DBSession.query(User) \
83 79
             .filter(User.email == 'an-other-email@test.local').one()
84 80
         ok_(user.validate_password('new-password'))
85
-        ok_(
86
-            webdav_digest != user.webdav_left_digest_response_hash,
87
-            msg='Webdav digest should be updated',
88
-        )

+ 0 - 5
tracim/tracim/tests/functional/test_user.py Zobrazit soubor

@@ -22,7 +22,6 @@ class TestAuthentication(TracimTestController):
22 22
 
23 23
         user = DBSession.query(User) \
24 24
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
25
-        webdav_digest = user.webdav_left_digest_response_hash
26 25
 
27 26
         try_post_user = self.app.post(
28 27
             '/user/{user_id}/password?_method=PUT'.format(
@@ -40,7 +39,3 @@ class TestAuthentication(TracimTestController):
40 39
         user = DBSession.query(User) \
41 40
             .filter(User.email == 'lawrence-not-real-email@fsf.local').one()
42 41
         ok_(user.validate_password('new-password'))
43
-        ok_(
44
-            webdav_digest != user.webdav_left_digest_response_hash,
45
-            msg='Webdav digest should be updated',
46
-        )

+ 44 - 0
tracim/tracim/tests/library/test_webdav.py Zobrazit soubor

@@ -481,6 +481,50 @@ class TestWebDav(TestStandard):
481 481
             )
482 482
         )
483 483
 
484
+    def test_unit__move_and_rename_content__ok(self):
485
+        provider = self._get_provider()
486
+        environ = self._get_environ(
487
+            provider,
488
+            'bob@fsf.local',
489
+        )
490
+        w1f1d1 = provider.getResourceInst(
491
+            '/w1/w1f1/w1f1d1.txt',
492
+            environ,
493
+        )
494
+
495
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
496
+            .filter(Content.label == 'w1f1d1') \
497
+            .one()  # It must exist only one revision, cf fixtures
498
+        ok_(content_w1f1d1, msg='w1f1d1 should be exist')
499
+        content_w1f1d1_id = content_w1f1d1.content_id
500
+        content_w1f1d1_parent = content_w1f1d1.parent
501
+        eq_(
502
+            content_w1f1d1_parent.label,
503
+            'w1f1',
504
+            msg='field parent should be w1f1',
505
+        )
506
+
507
+        w1f1d1.moveRecursive('/w1/w1f2/w1f1d1_RENAMED.txt')
508
+
509
+        # Database content is moved
510
+        content_w1f1d1 = DBSession.query(ContentRevisionRO) \
511
+            .filter(ContentRevisionRO.content_id == content_w1f1d1_id) \
512
+            .order_by(ContentRevisionRO.revision_id.desc()) \
513
+            .first()
514
+        ok_(
515
+            content_w1f1d1.parent.label != content_w1f1d1_parent.label,
516
+            msg='file should be moved in w1f2 but is in {0}'.format(
517
+                content_w1f1d1.parent.label
518
+            )
519
+        )
520
+        eq_(
521
+            'w1f1d1_RENAMED',
522
+            content_w1f1d1.label,
523
+            msg='File should be labeled w1f1d1_RENAMED, not {0}'.format(
524
+                content_w1f1d1.label
525
+            )
526
+        )
527
+
484 528
     def test_unit__move_content__ok__another_workspace(self):
485 529
         provider = self._get_provider()
486 530
         environ = self._get_environ(

+ 4 - 0
tracim/wsgidav.conf.sample Zobrazit soubor

@@ -25,6 +25,10 @@ manager_locks = True
25 25
 
26 26
 root_path = ''
27 27
 
28
+# Tracim doesn't support digest auth for webdav
29
+acceptbasic = True
30
+acceptdigest = False
31
+defaultdigest = False
28 32
 #===============================================================================
29 33
 # Lock Manager
30 34
 #