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