1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - ItemSupport library 4 5 @copyright: 2013, 2014 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from MoinMoin import config 10 from MoinMoin.Page import Page 11 from MoinMoin.PageEditor import PageEditor 12 from MoinMoin.security import Permissions 13 from MoinMoin.util import lock 14 from MoinSupport import getMetadata, getPagesForSearch 15 import os 16 17 # Content storage support. 18 19 class SpecialPermissionsForPage(Permissions): 20 21 "Permit saving of ACL-enabled pages." 22 23 def __init__(self, user, pagename): 24 Permissions.__init__(self, user) 25 self.pagename = pagename 26 27 def admin(self, pagename): 28 return pagename == self.pagename 29 30 write = admin 31 32 class ReadPermissionsForSubpages(Permissions): 33 34 "Permit listing of ACL-affected subpages." 35 36 def __init__(self, user, pagename): 37 Permissions.__init__(self, user) 38 self.pagename = pagename 39 40 def read(self, pagename): 41 return pagename.startswith("%s/" % self.pagename) 42 43 # Underlying storage mechanisms. 44 45 class GeneralItemStore: 46 47 "Common item store functionality." 48 49 def __init__(self, lock_dir): 50 51 "Initialise an item store with the given 'lock_dir' guarding access." 52 53 self.lock_dir = lock_dir 54 self.writelock = lock.WriteLock(lock_dir) 55 self.readlock = lock.ReadLock(lock_dir) 56 57 # High-level methods. 58 59 def __len__(self): 60 61 """ 62 Return the number of items. 63 """ 64 65 return len(self.keys()) 66 67 def __iter__(self): 68 69 "Return an iterator over the items in the store." 70 71 return ItemIterator(self) 72 73 def keys(self): 74 75 "Return a list of keys for items in the store." 76 77 self.readlock.acquire() 78 try: 79 return self.get_keys() 80 finally: 81 self.readlock.release() 82 83 def __getitem__(self, number): 84 85 "Return the item with the given 'number'." 86 87 self.readlock.acquire() 88 try: 89 try: 90 return self.read_item(number) 91 except (IOError, OSError): 92 raise IndexError, number 93 finally: 94 self.readlock.release() 95 96 def __delitem__(self, number): 97 98 "Remove the item with the given 'number' from the store." 99 100 self.writelock.acquire() 101 try: 102 try: 103 self.remove_item(number) 104 except (IOError, OSError): 105 raise IndexError, number 106 finally: 107 self.writelock.release() 108 109 def next(self): 110 111 """ 112 Return the number of the next item (which should also be the number of 113 items if none have been deleted). 114 """ 115 116 self.writelock.acquire() 117 try: 118 return self.get_next() 119 finally: 120 self.writelock.release() 121 122 class SequentialAccess(GeneralItemStore): 123 124 "Support sequential access to items." 125 126 def deduce_next(self): 127 128 "Deduce the next item number from the existing item files." 129 130 return max(self.get_keys() or [-1]) + 1 131 132 def read_next(self): 133 134 "Read the next item number from a special file." 135 136 if not os.path.exists(self.next_path): 137 return None 138 139 f = open(self.next_path) 140 try: 141 try: 142 return int(f.read()) 143 except ValueError: 144 return None 145 finally: 146 f.close() 147 148 def write_next(self, next): 149 150 "Write the 'next' item number to a special file." 151 152 f = open(self.next_path, "w") 153 try: 154 f.write(str(next)) 155 finally: 156 f.close() 157 158 class DirectoryStore(GeneralItemStore): 159 160 "A directory-based item store." 161 162 def __init__(self, path, lock_dir): 163 164 "Initialise an item store for the given 'path' and 'lock_dir'." 165 166 self.path = path 167 self.next_path = os.path.join(self.path, "next") 168 self.lock_dir = lock_dir 169 self.writelock = lock.WriteLock(lock_dir) 170 self.readlock = lock.ReadLock(lock_dir) 171 172 def mtime(self): 173 174 "Return the last modified time of the item store directory." 175 176 return os.path.getmtime(self.path) 177 178 def get_keys(self): 179 180 "Return the item keys." 181 182 return [int(filename) for filename in os.listdir(self.path) if filename.isdigit()] 183 184 def write_item(self, item, next): 185 186 "Write the given 'item' to a file with the given 'next' item number." 187 188 f = open(self.get_item_path(next), "wb") 189 try: 190 f.write(item) 191 finally: 192 f.close() 193 194 def read_item(self, identifier): 195 196 "Read the item with the given item 'identifier'." 197 198 f = open(self.get_item_path(identifier), "rb") 199 try: 200 return f.read() 201 finally: 202 f.close() 203 204 def remove_item(self, identifier): 205 206 "Remove the item with the given item 'identifier'." 207 208 os.remove(self.get_item_path(identifier)) 209 210 def get_item_path(self, identifier): 211 212 "Get the path for the given item 'identifier'." 213 214 if isinstance(identifier, unicode): 215 filename = identifier.encode(config.charset) 216 else: 217 filename = identifier 218 219 path = os.path.abspath(os.path.join(self.path, filename)) 220 basepath = os.path.join(self.path, "") 221 222 if os.path.commonprefix([path, basepath]) != basepath: 223 raise OSError, path 224 225 return path 226 227 class DirectoryItemStore(DirectoryStore, SequentialAccess): 228 229 "A directory-based item store with numeric keys." 230 231 def get_next(self): 232 233 "Return the next item number." 234 235 next = self.read_next() 236 if next is None: 237 next = self.deduce_next() 238 self.write_next(next) 239 return next 240 241 # High-level methods. 242 243 def append(self, item): 244 245 "Append the given 'item' to the store." 246 247 self.writelock.acquire() 248 try: 249 next = self.get_next() 250 self.write_item(item, next) 251 self.write_next(next + 1) 252 finally: 253 self.writelock.release() 254 255 class DirectoryNamedItemStore(DirectoryStore): 256 257 "A directory-based item store with explicit keys." 258 259 def __setitem__(self, name, item): 260 261 "Using the given 'name', set the given 'item' in the store." 262 263 self.writelock.acquire() 264 try: 265 self.write_item(item, name) 266 finally: 267 self.writelock.release() 268 269 class SubpageItemStore(SequentialAccess): 270 271 "A subpage-based item store." 272 273 def __init__(self, page, lock_dir): 274 275 "Initialise an item store for subpages under the given 'page'." 276 277 GeneralItemStore.__init__(self, lock_dir) 278 self.page = page 279 280 def mtime(self): 281 282 "Return the last modified time of the item store." 283 284 keys = self.get_keys() 285 if not keys: 286 page = self.page 287 else: 288 page = Page(self.page.request, self.get_item_path(max(keys))) 289 290 return wikiutil.version2timestamp( 291 getMetadata(page)["last-modified"] 292 ) 293 294 def get_next(self): 295 296 "Return the next item number." 297 298 return self.deduce_next() 299 300 def get_keys(self): 301 302 "Return the item keys." 303 304 request = self.page.request 305 306 # Collect the strict subpages of the parent page. 307 308 leafnames = [] 309 parentname = self.page.page_name 310 311 # To list pages whose ACLs may prevent access, a special policy is required. 312 313 may = request.user.may 314 request.user.may = ReadPermissionsForSubpages(request.user, parentname) 315 316 try: 317 for page in getPagesForSearch("title:regex:^%s/" % parentname, self.page.request): 318 basename, leafname = page.page_name.rsplit("/", 1) 319 320 # Only collect numbered pages immediately below the parent. 321 322 if basename == parentname and leafname.isdigit(): 323 leafnames.append(int(leafname)) 324 325 return leafnames 326 327 # Restore the original policy. 328 329 finally: 330 request.user.may = may 331 332 def write_item(self, item, next): 333 334 "Write the given 'item' to a page with the given 'next' item number." 335 336 request = self.page.request 337 pagename = self.get_item_path(next) 338 339 # To add a page with an ACL, a special policy is required. 340 341 may = request.user.may 342 request.user.may = SpecialPermissionsForPage(request.user, pagename) 343 344 # Attempt to save the page, copying any ACL. 345 346 try: 347 page = PageEditor(request, pagename) 348 page.saveText(item, 0) 349 350 # Restore the original policy. 351 352 finally: 353 request.user.may = may 354 355 def read_item(self, number): 356 357 "Read the item with the given item 'number'." 358 359 page = Page(self.page.request, self.get_item_path(number)) 360 return page.get_raw_body() 361 362 def remove_item(self, number): 363 364 "Remove the item with the given item 'number'." 365 366 page = PageEditor(self.page.request, self.get_item_path(number)) 367 page.deletePage() 368 369 def get_item_path(self, number): 370 371 "Get the path for the given item 'number'." 372 373 return "%s/%s" % (self.page.page_name, number) 374 375 # High-level methods. 376 377 def append(self, item): 378 379 "Append the given 'item' to the store." 380 381 self.writelock.acquire() 382 try: 383 next = self.get_next() 384 self.write_item(item, next) 385 finally: 386 self.writelock.release() 387 388 class ItemIterator: 389 390 "An iterator over items in a store." 391 392 def __init__(self, store, direction=1, keys=None): 393 self.store = store 394 self.direction = direction 395 self.keys = keys 396 self.reset() 397 398 def reset(self): 399 if self.direction == 1: 400 self._next = 0 401 self.final = self.store.next() 402 else: 403 self._next = self.store.next() - 1 404 self.final = 0 405 406 def more(self): 407 if self.direction == 1: 408 return self._next < self.final 409 else: 410 return self._next >= self.final 411 412 def get_next(self): 413 if self.keys: 414 next = self.keys[self._next] 415 else: 416 next = self._next 417 self._next += self.direction 418 return next 419 420 def next(self): 421 while self.more(): 422 try: 423 return self.store[self.get_next()] 424 except IndexError: 425 pass 426 427 raise StopIteration 428 429 def reverse(self): 430 self.direction = -self.direction 431 self.reset() 432 433 def reversed(self): 434 self.reverse() 435 return self 436 437 def __iter__(self): 438 return self 439 440 def getDirectoryItemStoreForPage(page, item_dir, lock_dir): 441 442 """ 443 A convenience function returning a directory-based store for the given 444 'page', using the given 'item_dir' and 'lock_dir'. 445 """ 446 447 item_dir_path = tuple(item_dir.split("/")) 448 lock_dir_path = tuple(lock_dir.split("/")) 449 return DirectoryItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) 450 451 def getDirectoryNamedItemStoreForPage(page, item_dir, lock_dir): 452 453 """ 454 A convenience function returning a directory-based store for the given 455 'page', using the given 'item_dir' and 'lock_dir'. 456 """ 457 458 item_dir_path = tuple(item_dir.split("/")) 459 lock_dir_path = tuple(lock_dir.split("/")) 460 return DirectoryNamedItemStore(page.getPagePath(*item_dir_path), page.getPagePath(*lock_dir_path)) 461 462 def getSubpageItemStoreForPage(page, lock_dir): 463 464 """ 465 A convenience function returning a subpage-based store for the given 466 'page', using the given 'lock_dir'. 467 """ 468 469 lock_dir_path = tuple(lock_dir.split("/")) 470 return SubpageItemStore(page, page.getPagePath(*lock_dir_path)) 471 472 # Page-oriented item store classes. 473 474 class ItemStoreBase: 475 476 "Access item stores via pages, observing page access restrictions." 477 478 def __init__(self, page, store): 479 self.page = page 480 self.store = store 481 482 def can_write(self): 483 484 """ 485 Return whether the user associated with the request can write to the 486 page owning this store. 487 """ 488 489 user = self.page.request.user 490 return user and user.may.write(self.page.page_name) 491 492 def can_read(self): 493 494 """ 495 Return whether the user associated with the request can read from the 496 page owning this store. 497 """ 498 499 user = self.page.request.user 500 return user and user.may.read(self.page.page_name) 501 502 def can_delete(self): 503 504 """ 505 Return whether the user associated with the request can delete the 506 page owning this store. 507 """ 508 509 user = self.page.request.user 510 return user and user.may.delete(self.page.page_name) 511 512 # Store-specific methods. 513 514 def mtime(self): 515 return self.store.mtime() 516 517 # High-level methods. 518 519 def keys(self): 520 521 "Return a list of keys for items in the store." 522 523 if not self.can_read(): 524 return 0 525 526 return self.store.keys() 527 528 def __len__(self): 529 530 "Return the number of items in the store." 531 532 if not self.can_read(): 533 return 0 534 535 return len(self.store) 536 537 def __getitem__(self, number): 538 539 "Return the item with the given 'number'." 540 541 if not self.can_read(): 542 raise IndexError, number 543 544 return self.store.__getitem__(number) 545 546 def __delitem__(self, number): 547 548 "Remove the item with the given 'number'." 549 550 if not self.can_delete(): 551 return 552 553 return self.store.__delitem__(number) 554 555 def __iter__(self): 556 return self.store.__iter__() 557 558 def next(self): 559 return self.store.next() 560 561 class SequentialStoreBase: 562 563 "Sequential access methods for item stores." 564 565 def append(self, item): 566 567 "Append the given 'item' to the store." 568 569 if not self.can_write(): 570 return 571 572 self.store.append(item) 573 574 class NamedStoreBase: 575 576 "Name-based access methods for item stores." 577 578 def __setitem__(self, name, item): 579 580 "Using the given 'name', set the given 'item' in the store." 581 582 if not self.can_write(): 583 return 584 585 self.store[name] = item 586 587 # Convenience store classes. 588 589 class ItemStore(ItemStoreBase, SequentialStoreBase): 590 591 "Store items in a directory via a page." 592 593 def __init__(self, page, item_dir="items", lock_dir=None): 594 ItemStoreBase.__init__(self, page, getDirectoryItemStoreForPage(page, item_dir, lock_dir or ("%s-locks" % item_dir))) 595 596 class NamedItemStore(ItemStoreBase, NamedStoreBase): 597 598 "Store items in a directory via a page." 599 600 def __init__(self, page, item_dir="items", lock_dir=None): 601 ItemStoreBase.__init__(self, page, getDirectoryNamedItemStoreForPage(page, item_dir, lock_dir or ("%s-locks" % item_dir))) 602 603 class ItemSubpageStore(ItemStoreBase): 604 605 "Store items in subpages of a page." 606 607 def __init__(self, page, lock_dir=None): 608 ItemStoreBase.__init__(self, page, getSubpageItemStoreForPage(page, lock_dir or "subpage-items-locks")) 609 610 # vim: tabstop=4 expandtab shiftwidth=4