anonymous group photoblog software
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1615 lines
58 KiB

  1. import os
  2. import re
  3. import time
  4. import sys
  5. import hashlib
  6. import mimetypes
  7. from subprocess import Popen, PIPE
  8. import misc
  9. import str_format
  10. import util
  11. import model
  12. import staff
  13. import staff_interface
  14. # NOTE: I'm not sure if interboard is a good module to have here.
  15. import interboard
  16. import config
  17. import strings as strings
  18. from util import WakaError, local
  19. from template import Template
  20. from wakapost import WakaPost
  21. try:
  22. import board_config_defaults
  23. except ImportError:
  24. board_config_defaults = None
  25. from sqlalchemy.sql import case, or_, and_, select, func, null
  26. class Board(object):
  27. def __init__(self, board):
  28. # Correct for missing key when running under WSGI
  29. if 'DOCUMENT_ROOT' not in local.environ:
  30. local.environ['DOCUMENT_ROOT'] = os.getcwd()
  31. # For WSGI mode (which does not initialize this for whatever reason).
  32. board_path = os.path.abspath(os.path.join(\
  33. local.environ['DOCUMENT_ROOT'],
  34. config.BOARD_DIR,
  35. board))
  36. if not os.path.exists(board_path):
  37. raise BoardNotFound()
  38. if not os.path.exists(os.path.join(board_path, 'board_config.py')):
  39. raise BoardNotFound('Board configuration not found.')
  40. module = util.import2('board_config', board_path)
  41. if board_config_defaults:
  42. self.options = board_config_defaults.config.copy()
  43. self.options.update(module.config)
  44. else:
  45. self.options = module.config
  46. self.table = model.board(self.options['SQL_TABLE'])
  47. # TODO likely will still need customization
  48. self.path = board_path
  49. url_path = os.path.join('/', os.path.relpath(\
  50. board_path,
  51. local.environ['DOCUMENT_ROOT']), '')
  52. self.url = str_format.percent_encode(url_path)
  53. self.name = board
  54. def make_path(self, file='', dir='', dirc=None, page=None, thread=None,
  55. ext=config.PAGE_EXT, abbr=False, hash=None, url=False,
  56. force_http=False):
  57. '''Builds an url or a path'''
  58. if url:
  59. base = self.url
  60. if force_http:
  61. base = 'http://' + local.environ['SERVER_NAME'] + base
  62. else:
  63. base = self.path
  64. if page is not None:
  65. if page == 0:
  66. file = self.options['HTML_SELF']
  67. ext = None
  68. else:
  69. file = str(page)
  70. if dirc:
  71. dir = self.options[dirc]
  72. if thread is not None:
  73. dir = self.options['RES_DIR']
  74. file = str(thread)
  75. if hash is not None:
  76. hash = '#%s' % hash
  77. else:
  78. hash = ''
  79. if file:
  80. if abbr:
  81. file += '_abbr'
  82. if ext is not None:
  83. file += '.' + ext.lstrip(".")
  84. return os.path.join(base, dir, file) + hash
  85. else:
  86. return os.path.join(base, dir) + hash
  87. def check_access(self, user):
  88. user.check_access(self.name)
  89. return user
  90. def make_url(self, **kwargs):
  91. '''Alias for make_path to build urls'''
  92. kwargs['url'] = True
  93. return self.make_path(**kwargs)
  94. def _get_all_threads(self):
  95. '''Build a list of threads from the database,
  96. where each thread is a list of WakaPost instances'''
  97. session = model.Session()
  98. table = self.table
  99. sql = table.select().order_by(table.c.stickied.desc(),
  100. table.c.lasthit.desc(),
  101. case({0: table.c.num}, table.c.parent, table.c.parent).asc(),
  102. table.c.num.asc()
  103. )
  104. query = session.execute(sql)
  105. threads = []
  106. thread = []
  107. for post in query:
  108. if thread and not post.parent:
  109. threads.append(thread)
  110. thread = []
  111. thread.append(WakaPost(post))
  112. threads.append(thread)
  113. return threads
  114. def get_some_threads(self, page):
  115. '''Grab a partial list of threads for pre-emptive pagination.'''
  116. session = model.Session()
  117. table = self.table
  118. thread_dict = {}
  119. thread_nums = []
  120. per_page = self.options['IMAGES_PER_PAGE']
  121. # Page is zero-indexed, so offset formula must differ.
  122. offset = page * per_page
  123. # Query 1: Grab all thread (OP) entries.
  124. op_sql = table.select().where(table.c.parent == 0).order_by(
  125. table.c.stickied.desc(),
  126. table.c.lasthit.desc(),
  127. table.c.num.asc()
  128. ).limit(per_page).offset(offset)
  129. op_query = session.execute(op_sql)
  130. for op in op_query:
  131. thread_dict[op.num] = [WakaPost(op)]
  132. thread_nums.append(op.num)
  133. # Query 2: Grab all reply entries and process.
  134. reply_sql = table.select().where(table.c.parent.in_(thread_nums))\
  135. .order_by(table.c.stickied.desc(),
  136. table.c.num.asc()
  137. )
  138. reply_query = session.execute(reply_sql)
  139. for post in reply_query:
  140. thread_dict[post.parent].append(WakaPost(post))
  141. return [thread_dict[num] for num in thread_nums]
  142. def build_cache(self):
  143. threads = self._get_all_threads()
  144. per_page = self.options['IMAGES_PER_PAGE']
  145. total = get_page_count(threads, per_page)
  146. for page in xrange(total):
  147. pagethreads = threads[page * per_page:\
  148. min(len(threads), (page + 1) * per_page)]
  149. self.build_cache_page(page, total, pagethreads)
  150. # check for and remove old pages
  151. page = total
  152. while os.path.exists(self.make_path(page=page)):
  153. os.unlink(self.make_path(page=page))
  154. page += 1
  155. if config.ENABLE_RSS:
  156. self.update_rss()
  157. def rebuild_cache(self):
  158. self.build_thread_cache_all()
  159. self.build_cache()
  160. def rebuild_cache_proxy(self, task_data):
  161. task_data.user.check_access(self.name)
  162. task_data.contents.append(self.name)
  163. Popen([sys.executable, sys.argv[0], 'rebuild_cache', self.name],
  164. env=util.proxy_environ())
  165. return util.make_http_forward(
  166. misc.make_script_url(task='mpanel', board=self.name),
  167. config.ALTERNATE_REDIRECT)
  168. def parse_page_threads(self, pagethreads):
  169. threads = []
  170. for postlist in pagethreads:
  171. if len(postlist) == 0:
  172. continue
  173. elif len(postlist) > 1:
  174. parent, replies = postlist[0], postlist[1:]
  175. else:
  176. parent, replies = postlist[0], []
  177. images = [x for x in replies if x.filename]
  178. if parent.stickied:
  179. max_replies = config.REPLIES_PER_STICKY
  180. else:
  181. max_replies = self.options['REPLIES_PER_THREAD']
  182. max_images = self.options['IMAGE_REPLIES_PER_THREAD'] \
  183. or len(images)
  184. thread = {}
  185. thread['omit'] = 0
  186. thread['omitimages'] = 0
  187. while len(replies) > max_replies or len(images) > max_images:
  188. post = replies.pop(0)
  189. thread['omit'] += 1
  190. if post.filename:
  191. thread['omitimages'] += 1
  192. thread['posts'] = [parent] + replies
  193. for post in thread['posts']:
  194. abbreviation = abbreviate_html(post.comment,
  195. self.options['MAX_LINES_SHOWN'],
  196. self.options['APPROX_LINE_LENGTH'])
  197. if abbreviation:
  198. post.abbrev = 1
  199. post.comment = abbreviation
  200. threads.append(thread)
  201. return threads
  202. def get_board_page_data(self, page, total, admin_page=''):
  203. if page >= total:
  204. if total:
  205. page = total - 1
  206. else:
  207. page = 0
  208. pages = []
  209. for i in xrange(total):
  210. p = {}
  211. p['page'] = i
  212. if admin_page:
  213. # Admin mode: direct to staff interface, not board pages.
  214. p['filename'] = misc.make_script_url(task=admin_page,
  215. board=self.name, page=i, _amp=True)
  216. else:
  217. p['filename'] = self.make_url(page=i)
  218. p['current'] = page == i
  219. pages.append(p)
  220. prevpage = nextpage = 'none'
  221. key_select = 'page' if admin_page else 'filename'
  222. if page != 0:
  223. prevpage = pages[page - 1][key_select]
  224. if page != total - 1 and total:
  225. nextpage = pages[page + 1][key_select]
  226. return (pages, prevpage, nextpage)
  227. def build_cache_page(self, page, total, pagethreads):
  228. '''Build $rootpath/$board/$page.html'''
  229. # Receive contents.
  230. threads = self.parse_page_threads(pagethreads)
  231. # Calculate page link data.
  232. (pages, prevpage, nextpage) = self.get_board_page_data(page, total)
  233. # Generate filename and links to other pages.
  234. filename = self.make_path(page=page)
  235. Template('page_template',
  236. pages=pages,
  237. postform=self.options['ALLOW_TEXTONLY'] \
  238. or self.options['ALLOW_IMAGES'],
  239. image_inp=self.options['ALLOW_IMAGES'],
  240. textonly_inp=(self.options['ALLOW_IMAGES'] \
  241. and self.options['ALLOW_TEXTONLY']),
  242. prevpage=prevpage,
  243. nextpage=nextpage,
  244. threads=threads,
  245. ).render_to_file(filename)
  246. def get_thread_posts(self, threadid):
  247. session = model.Session()
  248. sql = self.table.select(
  249. or_(
  250. self.table.c.num == threadid,
  251. self.table.c.parent == threadid
  252. )).order_by(self.table.c.num.asc())
  253. query = session.execute(sql)
  254. thread = []
  255. for post in query:
  256. thread.append(WakaPost(post))
  257. if not len(thread):
  258. raise WakaError('Thread not found.')
  259. if thread[0].parent:
  260. raise WakaError(strings.NOTHREADERR)
  261. return thread
  262. def build_thread_cache(self, threadid):
  263. '''Build $rootpath/$board/$res/$threadid.html'''
  264. thread = self.get_thread_posts(threadid)
  265. filename = os.path.join(self.path, self.options['RES_DIR'],
  266. "%s%s" % (threadid, config.PAGE_EXT))
  267. def print_thread(thread, filename, **kwargs):
  268. '''Function to avoid duplicating code with abbreviated pages'''
  269. Template('page_template',
  270. threads=[{'posts': thread}],
  271. thread=threadid,
  272. postform=self.options['ALLOW_TEXT_REPLIES'] \
  273. or self.options['ALLOW_IMAGE_REPLIES'],
  274. image_inp=self.options['ALLOW_IMAGE_REPLIES'],
  275. textonly_inp=0,
  276. dummy=thread[-1].num,
  277. lockedthread=thread[0].locked,
  278. **kwargs
  279. ).render_to_file(filename)
  280. print_thread(thread, filename)
  281. # Determine how many posts need to be cut.
  282. posts_to_trim = len(thread) - config.POSTS_IN_ABBREVIATED_THREAD_PAGES
  283. # Filename for Last xx Posts Page.
  284. abbreviated_filename = os.path.join(self.path,
  285. self.options['RES_DIR'],
  286. "%s_abbr%s" % (threadid, config.PAGE_EXT))
  287. if config.ENABLE_ABBREVIATED_THREAD_PAGES and posts_to_trim > 1:
  288. op = thread[0]
  289. thread = thread[posts_to_trim:]
  290. thread.insert(0, op)
  291. if len(thread) > 1:
  292. min_res = thread[1].num
  293. else:
  294. min_res = op.num
  295. print_thread(thread, abbreviated_filename,
  296. omit=posts_to_trim - 1, min_res=min_res)
  297. else:
  298. if os.path.exists(abbreviated_filename):
  299. os.unlink(abbreviated_filename)
  300. def delete_thread_cache(self, parent, archiving):
  301. archive_dir = self.options['ARCHIVE_DIR']
  302. base = os.path.join(self.path, self.options['RES_DIR'], '')
  303. full_filename = "%s%s" % (parent, config.PAGE_EXT)
  304. full_thread_page = base + full_filename
  305. abbrev_thread_page = base + "%s_abbr%s" % (parent, config.PAGE_EXT)
  306. if archiving:
  307. archive_base = os.path.join(self.path,
  308. self.options['ARCHIVE_DIR'],
  309. self.options['RES_DIR'], '')
  310. try:
  311. os.makedirs(archive_base, 0755)
  312. except os.error:
  313. pass
  314. archive_thread_page = archive_base + full_filename
  315. with open(full_thread_page, 'r') as res_in:
  316. with open(archive_thread_page, 'w') as res_out:
  317. for line in res_in:
  318. # Update thumbnail links.
  319. line = re.sub(r'img src="(.*?)'
  320. + self.options['THUMB_DIR'],
  321. r'img src="\1'
  322. + os.path.join(archive_dir,
  323. self.options['THUMB_DIR'], ''),
  324. line)
  325. # Update image links.
  326. line = re.sub(r'a href="(.*?)'
  327. + self.options['IMG_DIR'],
  328. r'a href="\1'
  329. + os.path.join(archive_dir,
  330. self.options['IMG_DIR'], ''),
  331. line)
  332. # Update reply links.
  333. line = re.sub(r'a href="(.*?)'
  334. + os.path.join(self.path,
  335. self.options['RES_DIR'], ''),
  336. r'a href="\1' + os.path.join(\
  337. self.path,
  338. self.options['RES_DIR'], ''),
  339. line)
  340. res_out.write(line)
  341. if os.path.exists(full_thread_page):
  342. os.unlink(full_thread_page)
  343. if os.path.exists(abbrev_thread_page):
  344. os.unlink(abbrev_thread_page)
  345. def build_thread_cache_all(self):
  346. session = model.Session()
  347. sql = select([self.table.c.num], self.table.c.parent == 0)
  348. query = session.execute(sql)
  349. for row in query:
  350. self.build_thread_cache(row[0])
  351. def _handle_post(self, wakapost, editing=None, admin_data=None):
  352. """Worst function ever"""
  353. session = model.Session()
  354. # get a timestamp for future use
  355. timestamp = time.time()
  356. if admin_data:
  357. admin_data.user.check_access(self.name)
  358. wakapost.admin_post = True
  359. # run several post validations - raises exceptions
  360. wakapost.validate(editing, admin_data, self.options)
  361. # check whether the parent thread is stickied
  362. if wakapost.parent:
  363. self.sticky_lock_check(wakapost, admin_data)
  364. self.sticky_lock_update(wakapost.parent, wakapost.stickied,
  365. wakapost.locked)
  366. ip = local.environ['REMOTE_ADDR']
  367. numip = misc.dot_to_dec(ip)
  368. wakapost.set_ip(numip, editing)
  369. # set up cookies
  370. wakapost.make_post_cookies(self.options, self.url)
  371. # check if IP is whitelisted
  372. whitelisted = misc.is_whitelisted(numip)
  373. if not whitelisted and not admin_data:
  374. # check for bans
  375. interboard.ban_check(numip, wakapost.name,
  376. wakapost.subject, wakapost.comment)
  377. # check for spam matches
  378. trap_fields = []
  379. if self.options['SPAM_TRAP']:
  380. trap_fields = ['name', 'link']
  381. misc.spam_engine(trap_fields, config.SPAM_FILES)
  382. # check for open proxies
  383. if self.options['ENABLE_PROXY_CHECK']:
  384. self.proxy_check(ip)
  385. # check if thread exists, and get lasthit value
  386. parent_res = None
  387. if not editing:
  388. wakapost.timestamp = timestamp
  389. if wakapost.parent:
  390. parent_res = self.get_parent_post(wakapost.parent)
  391. if not parent_res:
  392. raise WakaError(strings.NOTHREADERR)
  393. wakapost.lasthit = parent_res.lasthit
  394. else:
  395. wakapost.lasthit = timestamp
  396. # split tripcode and name
  397. wakapost.set_tripcode(self.options['TRIPKEY'])
  398. # clean fields
  399. wakapost.clean_fields(editing, admin_data, self.options)
  400. # flood protection - must happen after inputs have been cleaned up
  401. self.flood_check(numip, timestamp, wakapost.comment,
  402. wakapost.req_file, editing is None, False)
  403. # generate date
  404. wakapost.set_date(editing, self.options['DATE_STYLE'])
  405. # generate ID code if enabled
  406. if self.options['DISPLAY_ID']:
  407. wakapost.date += ' ID:' + \
  408. self.make_id_code(ip, timestamp, wakapost.email)
  409. # copy file, do checksums, make thumbnail, etc
  410. if wakapost.req_file:
  411. if editing and (editing.filename or editing.thumbnail):
  412. self.delete_file(editing.filename, editing.thumbnail)
  413. # TODO: this process_file is just a thin wrapper around awful code
  414. wakapost.process_file(self, editing is not None)
  415. # choose whether we need an SQL UPDATE (editing) or INSERT (posting)
  416. if editing:
  417. db_update = self.table.update().where(
  418. self.table.c.num == wakapost.num)
  419. else:
  420. db_update = self.table.insert()
  421. db_update = db_update.values(**wakapost.db_values)
  422. # finally, write to the database
  423. result = session.execute(db_update)
  424. if not editing:
  425. if wakapost.parent:
  426. self.update_bump(wakapost, parent_res)
  427. wakapost.num = result.inserted_primary_key[0]
  428. # remove old threads from the database
  429. self.trim_database()
  430. # update the cached HTML pages
  431. self.build_cache()
  432. # update the individual thread cache
  433. self.build_thread_cache(wakapost.parent or wakapost.num)
  434. return wakapost.num
  435. def post_stuff(self, wakapost, admin_data=None):
  436. # For use with noko, below.
  437. parent = wakapost.parent or wakapost.num
  438. noko = wakapost.noko
  439. try:
  440. post_num = self._handle_post(wakapost, admin_data=admin_data)
  441. except util.SpamError:
  442. forward = self.make_path(page=0, url=True)
  443. return util.make_http_forward(forward, config.ALTERNATE_REDIRECT)
  444. forward = ''
  445. if not admin_data:
  446. if not noko:
  447. # forward back to the main page
  448. forward = self.make_path(page=0, url=True)
  449. else:
  450. # ...unless we have "noko" (a la 4chan)--then forward to
  451. # thread ("parent" contains current post number if a new
  452. # thread was posted)
  453. if not os.path.exists(self.make_path(thread=parent,
  454. abbr=True)):
  455. forward = self.make_url(thread=parent)
  456. else:
  457. forward = self.make_url(thread=parent, abbr=True)
  458. else:
  459. # forward back to the mod panel
  460. kwargs = dict(task='mpanel', board=self.name)
  461. if noko:
  462. kwargs['page'] = "t%s" % parent
  463. forward = misc.make_script_url(**kwargs)
  464. admin_data.contents.append('/%s/%d' % (self.name, post_num))
  465. return util.make_http_forward(forward, config.ALTERNATE_REDIRECT)
  466. # end of this function. fuck yeah
  467. def edit_gateway_window(self, post_num):
  468. return self._gateway_window(post_num, 'edit')
  469. def delete_gateway_window(self, post_num):
  470. return self._gateway_window(post_num, 'delete')
  471. def _gateway_window(self, post_num, task):
  472. if not post_num.isdigit():
  473. raise WakaError('Please enter post number.')
  474. wakapost = self.get_post(post_num)
  475. if not wakapost:
  476. raise WakaError(strings.POSTNOTFOUND)
  477. template_name = 'password' if task == 'edit' else 'delpassword'
  478. return Template(template_name, admin_post=wakapost.admin_post, num=post_num)
  479. def get_local_reports(self):
  480. session = model.Session()
  481. table = model.report
  482. sql = table.select().where(and_(table.c.board == self.name,
  483. table.c.resolved == 0))
  484. query = session.execute(sql).fetchall()
  485. reported_posts = [dict(row.items()) for row in query]
  486. rowtype = 1
  487. for row in reported_posts:
  488. # Alternate between rowtypes 1 and 2.
  489. rowtype ^= 0x3
  490. row['rowtype'] = rowtype
  491. return reported_posts
  492. def delete_by_ip(self, task_data, ip, mask='255.255.255.255'):
  493. if task_data and not task_data.contents:
  494. task_data.contents.append(ip + ' (' + mask + ')' + ' @ ' \
  495. + self.name)
  496. try:
  497. ip = int(ip)
  498. except ValueError:
  499. ip = misc.dot_to_dec(ip)
  500. try:
  501. mask = int(mask)
  502. except ValueError:
  503. mask = misc.dot_to_dec(mask or '255.255.255.255')
  504. session = model.Session()
  505. table = self.table
  506. sql = table.select().where(and_(
  507. table.c.ip.op('&')(mask) == ip & mask,
  508. table.c.timestamp > (time.time() - config.NUKE_TIME_THRESHOLD)
  509. ))
  510. rows = session.execute(sql)
  511. if not rows.rowcount:
  512. return
  513. timestamp = None
  514. if config.POST_BACKUP:
  515. timestamp = time.time()
  516. for row in rows:
  517. try:
  518. self.delete_post(row.num, '', False, False, admin=True,
  519. timestampofarchival=timestamp)
  520. except WakaError:
  521. pass
  522. self.build_cache()
  523. def delete_stuff(self, posts, password, file_only, archiving,
  524. caller='user', admindelete=False,
  525. admin_data=None, from_window=False):
  526. if caller == 'internal':
  527. # Internally called; force admin.
  528. admindelete = True
  529. timestamp = None
  530. if config.POST_BACKUP:
  531. timestamp = time.time()
  532. for post in posts:
  533. self.delete_post(post, password, file_only, archiving,
  534. from_window=False, admin=admindelete,
  535. timestampofarchival=timestamp,
  536. admin_data=admin_data)
  537. self.build_cache()
  538. if admindelete:
  539. forward = misc.make_script_url(task='mpanel', board=self.name)
  540. else:
  541. forward = self.make_path(page=0, url=True)
  542. if caller == 'user':
  543. return util.make_http_forward(forward, config.ALTERNATE_REDIRECT)
  544. def delete_post(self, post, password, file_only, archiving,
  545. admin_data=None, from_window=False, admin=False,
  546. timestampofarchival=None, recur=False):
  547. '''Delete a single post from the board. This method does not rebuild
  548. index cache automatically.'''
  549. session = model.Session()
  550. table = self.table
  551. row = self.get_post(post)
  552. if row is None:
  553. raise WakaError(strings.POSTNOTFOUND % (int(post), self.name))
  554. if not admin:
  555. archiving = False
  556. if row.admin_post:
  557. raise WakaError(strings.MODDELETEONLY)
  558. if password != row.password:
  559. raise WakaError("Post #%s: %s" % (post, strings.BADDELPASS))
  560. if config.POST_BACKUP and not archiving:
  561. if not timestampofarchival:
  562. timestampofarchival = time.time()
  563. sql = model.backup.insert().values(board_name=self.name,
  564. postnum=row.num,
  565. parent=row.parent,
  566. timestamp=row.timestamp,
  567. lasthit=row.lasthit,
  568. ip=row.ip,
  569. date=row.date,
  570. name=row.name,
  571. trip=row.trip,
  572. email=row.email,
  573. subject=row.subject,
  574. password=row.password,
  575. comment=row.comment,
  576. image=row.filename,
  577. size=row.size,
  578. md5=row.md5,
  579. width=row.width,
  580. height=row.height,
  581. thumbnail=row.thumbnail,
  582. tn_width=row.tn_width,
  583. tn_height=row.tn_height,
  584. lastedit=row.lastedit,
  585. lastedit_ip=row.lastedit_ip,
  586. admin_post=row.admin_post,
  587. stickied=row.stickied,
  588. locked=row.locked,
  589. timestampofarchival=\
  590. timestampofarchival)
  591. session.execute(sql)
  592. if file_only:
  593. # remove just the image and update the database
  594. select_post_image = select([table.c.image, table.c.thumbnail],
  595. or_(table.c.num == post))
  596. baleet_me = session.execute(select_post_image).fetchone()
  597. if baleet_me.image and baleet_me.thumbnail:
  598. self.delete_file(baleet_me.image, baleet_me.thumbnail,
  599. archiving=archiving)
  600. postupdate = table.update().where(table.c.num == post).values(
  601. size=0, md5=null(), thumbnail=null())
  602. session.execute(postupdate)
  603. else:
  604. if config.POST_BACKUP and not archiving:
  605. select_thread_images \
  606. = select([table.c.image, table.c.thumbnail],
  607. table.c.num == post)
  608. else:
  609. select_thread_images \
  610. = select([table.c.image, table.c.thumbnail],
  611. or_(table.c.num == post, table.c.parent == post))
  612. images_to_baleet = session.execute(select_thread_images)
  613. for i in images_to_baleet:
  614. if i.image and i.thumbnail:
  615. self.delete_file(i.image, i.thumbnail, archiving=archiving)
  616. if config.POST_BACKUP and not archiving:
  617. delete_query = table.delete(table.c.num == post)
  618. else:
  619. delete_query = table.delete(or_(
  620. table.c.num == post, table.c.parent == post))
  621. session.execute(delete_query)
  622. # Also back-up child posts.
  623. if config.POST_BACKUP and not archiving:
  624. sql = select([table.c.num], table.c.parent == post)
  625. sel_posts = session.execute(sql).fetchall()
  626. for i in [p[0] for p in sel_posts]:
  627. self.delete_post(i, '', False, False,
  628. from_window=from_window, admin=True,
  629. recur=True)
  630. # Cache building
  631. if not row.parent:
  632. if file_only:
  633. # removing parent (OP) image
  634. self.build_thread_cache(post)
  635. else:
  636. # removing an entire thread
  637. self.delete_thread_cache(post, archiving)
  638. elif not recur:
  639. # removing a reply, or a reply's image
  640. self.build_thread_cache(row.parent)
  641. if admin_data:
  642. admin_data.contents.append('/%s/%d' % (self.name, int(post)))
  643. def delete_file(self, relative_file_path, relative_thumb_path,
  644. archiving=False):
  645. full_file_path = os.path.join(self.path, relative_file_path)
  646. full_thumb_path = os.path.join(self.path, relative_thumb_path)
  647. archive_base = os.path.join(self.path,
  648. self.options['ARCHIVE_DIR'], '')
  649. backup_base = os.path.join(archive_base, self.options['BACKUP_DIR'])
  650. if config.POST_BACKUP:
  651. try:
  652. os.makedirs(backup_base, 0755)
  653. except os.error:
  654. pass
  655. full_archive_path = os.path.join(archive_base,
  656. relative_file_path)
  657. full_tarchive_path = os.path.join(archive_base,
  658. relative_thumb_path)
  659. full_backup_path = os.path.join(backup_base,
  660. os.path.basename(relative_file_path))
  661. full_tbackup_path = os.path.join(backup_base,
  662. os.path.basename(relative_thumb_path))
  663. if os.path.exists(full_file_path):
  664. if archiving:
  665. os.renames(full_file_path, full_archive_path)
  666. os.chmod(full_archive_path, 0644)
  667. elif config.POST_BACKUP:
  668. os.renames(full_file_path, full_backup_path)
  669. os.chmod(full_backup_path, 0644)
  670. else:
  671. os.unlink(full_file_path)
  672. if os.path.exists(full_thumb_path) \
  673. and re.match(self.options['THUMB_DIR'], relative_file_path):
  674. if archiving:
  675. os.renames(full_thumb_path, full_tarchive_path)
  676. os.chmod(full_tarchive_path, 0644)
  677. elif config.POST_BACKUP:
  678. os.renames(full_thumb_path, full_tbackup_path)
  679. os.chmod(full_tbackup_path, 0644)
  680. else:
  681. os.unlink(full_thumb_path)
  682. def remove_backup_stuff(self, admin_data, posts, restore=False):
  683. user = admin_data.user
  684. user.check_access(self.name)
  685. if restore:
  686. admin_data.action = 'backup_restore'
  687. for post in posts:
  688. self.remove_backup_post(admin_data, post, restore=restore)
  689. # Log.
  690. admin_data.contents.append('/%s/%d' % (self.name, int(post)))
  691. # Board pages need refereshing.
  692. self.build_cache()
  693. return staff_interface.StaffInterface(user.login_data.cookie,
  694. board=self,
  695. dest=staff_interface.TRASH_PANEL)
  696. def remove_backup_post(self, task_data, post, restore=False, child=False):
  697. session = model.Session()
  698. table = model.backup
  699. sql = table.select().where(and_(table.c.postnum == post,
  700. table.c.board_name == self.name))
  701. row = session.execute(sql).fetchone()
  702. if not row:
  703. raise WakaError('Backup record not found for post %s.' % (post))
  704. arch_dir = os.path.join(self.path,
  705. self.options['ARCHIVE_DIR'],
  706. self.options['BACKUP_DIR'], '')
  707. if row.image:
  708. arch_image = os.path.join(arch_dir, os.path.basename(row.image))
  709. else:
  710. arch_image = None
  711. if row.thumbnail:
  712. arch_thumb = os.path.join(arch_dir, os.path.basename(row.thumbnail))
  713. else:
  714. arch_thumb = None
  715. if restore:
  716. my_table = self.table
  717. if row.parent and not child:
  718. sql = my_table.select().where(my_table.c.num == row.parent)
  719. parent = session.execute(sql).fetchone()
  720. if not parent:
  721. raise WakaError('Cannot restore post %s: '
  722. 'Parent thread deleted.' % (post))
  723. stickied = parent.stickied
  724. locked = parent.locked
  725. lasthit = parent.lasthit
  726. else:
  727. stickied = row.stickied
  728. locked = row.locked
  729. lasthit = row.lasthit
  730. # Perform insertion.
  731. sql = my_table.insert().values(num=row.postnum,
  732. parent=row.parent,
  733. timestamp=row.timestamp,
  734. lasthit=lasthit,
  735. ip=row.ip,
  736. date=row.date,
  737. name=row.name,
  738. trip=row.trip,
  739. email=row.email,
  740. subject=row.subject,
  741. password=row.password,
  742. comment=row.comment,
  743. image=row.image,
  744. size=row.size,
  745. md5=row.md5,
  746. width=row.width,
  747. height=row.height,
  748. thumbnail=row.thumbnail,
  749. tn_width=row.tn_width,
  750. tn_height=row.tn_height,
  751. lastedit=row.lastedit,
  752. lastedit_ip=row.lastedit_ip,
  753. admin_post=row.admin_post,
  754. stickied=stickied,
  755. locked=locked)
  756. session.execute(sql)
  757. # Move file/thumb.
  758. if arch_image and os.path.exists(arch_image):
  759. orig_path = os.path.join(self.path, row.image)
  760. os.renames(arch_image, os.path.join(self.path, row.image))
  761. os.chmod(orig_path, 0644)
  762. if arch_thumb \
  763. and re.match(self.options['THUMB_DIR'],
  764. row.thumbnail) \
  765. and os.path.exists(arch_thumb):
  766. os.renames(arch_thumb, os.path.join(self.path, row.thumb))
  767. if not child:
  768. if row.parent:
  769. self.build_thread_cache(row.parent)
  770. else:
  771. self.build_thread_cache(row.postnum)
  772. else:
  773. # Delete file/thumb.
  774. if arch_image and os.path.exists(arch_image):
  775. os.unlink(os.path.join(arch_image))
  776. if arch_thumb \
  777. and re.match(self.options['THUMB_DIR'],
  778. row.thumbnail) \
  779. and os.path.exists(arch_thumb):
  780. os.unlink(arch_thumb)
  781. # Remove (and restore if appropriate) all thread backups made at the
  782. # point of archival.
  783. if not row.parent:
  784. sql = table.select(and_(table.c.parent == row.postnum,
  785. table.c.board_name == self.name,
  786. table.c.timestampofarchival\
  787. == row.timestampofarchival))\
  788. .order_by(table.c.num.asc())
  789. for row in session.execute(sql):
  790. self.remove_backup_post(None, row.postnum, restore=restore,
  791. child=True)
  792. sql = table.delete().where(and_(table.c.postnum == post,
  793. table.c.board_name == self.name))
  794. session.execute(sql)
  795. def make_report_post_window(self, posts, from_window=False):
  796. if len(posts) == 0:
  797. raise WakaError('No posts selected.')
  798. if len(posts) > 10:
  799. raise WakaError('Too many posts. Try reporting the thread ' \
  800. + 'or a single post in the case of floods.')
  801. num_parsed = ', '.join(posts)
  802. referer = ''
  803. if not from_window:
  804. referer = self.url
  805. return Template('post_report_window', num=num_parsed, referer=referer)
  806. def report_posts(self, comment, referer, posts):
  807. numip = misc.dot_to_dec(local.environ['REMOTE_ADDR'])
  808. # Sanity checks.
  809. if not comment:
  810. raise WakaError('Please input a comment.')
  811. if len(comment) > config.REPORT_COMMENT_MAX_LENGTH:
  812. raise WakaError('Comment is too long.')
  813. if len(comment) < 3:
  814. raise WakaError('Comment is too short.')
  815. if len(posts) > 10:
  816. raise WakaError('Too many posts. Try reporting the thread or a '\
  817. + 'single post in the case of floods.')
  818. # Access checks.
  819. whitelisted = misc.is_whitelisted(numip)
  820. if not whitelisted:
  821. interboard.ban_check(numip, '', '', '')
  822. self.flood_check(numip, time.time(), comment, '', False, True)
  823. # Clear up the backlog.
  824. interboard.trim_reported_posts()
  825. comment = str_format.format_comment(str_format.clean_string(
  826. str_format.decode_string(comment)))
  827. session = model.Session()
  828. reports_table = model.report
  829. # Handle errors individually rather than cancelling operation.
  830. errors = []
  831. for post in posts:
  832. if not post.isdigit():
  833. errors.append({'error' : '%s: Invalid post number.' % (post)})
  834. continue
  835. sql = select([self.table.c.ip], self.table.c.num == post,
  836. self.table)
  837. post_row = session.execute(sql).fetchone()
  838. if not post_row:
  839. errors.append({'error' \
  840. : '%s: Post not found (likely deleted).' % (post) })
  841. continue
  842. # Store offender IP in case this post is deleted later.
  843. offender_ip = post_row.ip
  844. sql = reports_table.select()\
  845. .where(and_(reports_table.c.postnum == post,
  846. reports_table.c.board == self.name))
  847. report_row = session.execute(sql).fetchone()
  848. if report_row:
  849. if report_row['resolved']:
  850. errors.append({'error' : '%s: Already resolved.' \
  851. % (post)})
  852. else:
  853. errors.append({'error' : '%s: Already reported.' \
  854. % (post)})
  855. continue
  856. timestamp = time.time()
  857. date = misc.make_date(timestamp, self.options['DATE_STYLE'])
  858. # File report.
  859. sql = reports_table.insert().values(reporter=numip,
  860. board=self.name,
  861. offender=offender_ip,
  862. postnum=post,
  863. comment=comment,
  864. timestamp=timestamp,
  865. date=date,
  866. resolved=0)
  867. session.execute(sql)
  868. return Template('report_submitted', errors=errors,
  869. error_occurred=len(errors)>0,
  870. referer=referer)
  871. def edit_window(self, post_num, cookie, password, admin_mode=False):
  872. wakapost = self.get_post(post_num)
  873. if wakapost is None:
  874. raise WakaError('Post not found')
  875. if admin_mode:
  876. staff.StaffMember.get_from_cookie(cookie).check_access(self)
  877. elif password != wakapost.password:
  878. raise WakaError('Wrong pass for editing') # TODO
  879. return Template('post_edit_template', loop=[wakapost],
  880. admin=admin_mode)
  881. def edit_stuff(self, request_post, admin_data=None):
  882. original_post = self.get_post(request_post.num)
  883. if not original_post:
  884. raise WakaError('Post not found')
  885. if not admin_data and request_post.password != original_post.password:
  886. raise WakaError('Wrong password for editing')
  887. edited_post = WakaPost.copy(original_post)
  888. edited_post.merge(request_post, which='request')
  889. if edited_post.killtrip:
  890. edited_post.trip = ''
  891. try:
  892. self._handle_post(edited_post, original_post, admin_data)
  893. except util.SpamError:
  894. return Template('edit_successful')
  895. if admin_data:
  896. admin_data.contents.append(
  897. '/%s/%d' % (self.name, int(request_post.num)))
  898. return Template('edit_successful')
  899. def process_file(self, filestorage, timestamp, parent, editing):
  900. filetypes = self.options.get('EXTRA_FILETYPES', [])
  901. # analyze file and check that it's in a supported format
  902. ext, width, height = misc.analyze_image(filestorage.stream,
  903. filestorage.filename)
  904. known = (width != 0 or ext in filetypes)
  905. if not (self.options['ALLOW_UNKNOWN'] or known) or \
  906. ext in self.options['FORBIDDEN_EXTENSIONS']:
  907. raise WakaError(strings.BADFORMAT)
  908. maxw, maxh, maxp = self.options['MAX_IMAGE_WIDTH'], \
  909. self.options['MAX_IMAGE_HEIGHT'], self.options['MAX_IMAGE_PIXELS']
  910. if (maxw and width > maxw) or (maxh and height > maxh) or \
  911. (maxp and (width * height) > maxp):
  912. raise WakaError(strings.BADFORMAT)
  913. # generate "random" filename
  914. filebase = ("%.3f" % timestamp).replace(".", "")
  915. filename = self.make_path(filebase, dirc='IMG_DIR', ext=ext)
  916. if not known:
  917. filename += self.options['MUNGE_UNKNOWN']
  918. # copy file
  919. try:
  920. filestorage.save(filename)
  921. except IOError:
  922. raise WakaError(strings.NOTWRITE)
  923. # Check file type with UNIX utility file()
  924. file_response = Popen(["file", filename], stdout=PIPE)\
  925. .communicate()[0]
  926. if re.match("\:.*(?:script|text|executable)", file_response):
  927. os.unlink(filename)
  928. raise WakaError(strings.BADFORMAT + " Potential Exploit")
  929. # Generate thumbnail based on file
  930. if file_response.find('JPEG') != -1:
  931. thumb_ext = 'jpg'
  932. elif file_response.find('GIF') != -1:
  933. thumb_ext = 'gif'
  934. elif file_response.find('PNG') != -1:
  935. thumb_ext = 'png'
  936. else:
  937. thumb_ext = os.path.splitext(filename)[1]
  938. thumbnail = self.make_path(filebase + "s", dirc='THUMB_DIR',
  939. ext=thumb_ext)
  940. # get the checksum
  941. md5h = hashlib.md5()
  942. filestorage.stream.seek(0)
  943. while True:
  944. buffer = filestorage.stream.read(16 * 1024)
  945. if not buffer:
  946. break
  947. md5h.update(buffer)
  948. md5 = md5h.hexdigest()
  949. # check for duplicate files
  950. if (not editing and \
  951. (parent and self.options['DUPLICATE_DETECTION'] == 'thread') or
  952. self.options['DUPLICATE_DETECTION'] == 'board'):
  953. session = model.Session()
  954. # Check dupes in same thread
  955. if self.options['DUPLICATE_DETECTION'] == 'thread':
  956. sql = self.table.select("md5=:md5 AND (parent=:parent OR "
  957. "num=:num)").params(md5=md5, parent=parent, num=parent)
  958. result = session.execute(sql)
  959. else: # Check dupes throughout board
  960. sql = self.table.select("md5=:md5").params(md5=md5)
  961. result = session.execute(sql)
  962. match = result.fetchone()
  963. if match:
  964. os.unlink(filename) # make sure to remove the file
  965. raise WakaError(strings.DUPE %
  966. self.get_reply_link(match['num'], parent))
  967. # do thumbnail
  968. tn_width = tn_height = 0
  969. tn_ext = ''
  970. if not width: # unsupported file
  971. if ext in filetypes: # externally defined filetype
  972. # Compensate for absolute paths, if given.
  973. icon = config.ICONS.get(ext, None)
  974. if icon:
  975. if icon.startswith('/'):
  976. icon = os.path.join(local.environ['DOCUMENT_ROOT'],
  977. icon.lstrip("/"))
  978. else:
  979. icon = os.path.join(self.path, icon)
  980. if icon and os.path.exists(icon):
  981. tn_ext, tn_width, tn_height = \
  982. misc.analyze_image(open(icon, "rb"), icon)
  983. else:
  984. tn_ext, tn_width, tn_height = ('', 0, 0)
  985. # was that icon file really there?
  986. if tn_width:
  987. thumbnail = icon
  988. else:
  989. thumbnail = ''
  990. else:
  991. thumbnail = ''
  992. elif width > self.options['MAX_W'] or \
  993. height > self.options['MAX_H'] or \
  994. self.options['THUMBNAIL_SMALL']:
  995. if width <= self.options['MAX_W'] and \
  996. height <= self.options['MAX_H']:
  997. tn_width = width
  998. tn_height = height
  999. else:
  1000. tn_width = self.options['MAX_W']
  1001. tn_height = height * self.options['MAX_W'] / width
  1002. if tn_height > self.options['MAX_H']:
  1003. tn_width = width * self.options['MAX_H'] / height
  1004. tn_height = self.options['MAX_H']
  1005. if self.options['STUPID_THUMBNAILING']:
  1006. thumbnail = filename
  1007. else:
  1008. tn_width, tn_height \
  1009. = misc.make_thumbnail(filename, thumbnail, tn_width,
  1010. tn_height, self.options['THUMBNAIL_QUALITY'],
  1011. self.options['CONVERT_COMMAND'])
  1012. if not tn_width and tn_height:
  1013. thumbnail = ''
  1014. else:
  1015. tn_width = width
  1016. tn_height = height
  1017. thumbnail = filename
  1018. # restore the name for extensions in KEEP_NAME_FILETYPES
  1019. # or, if it doesn't exist, for all the ones in filetypes
  1020. if ext in self.options.get('KEEP_NAME_FILETYPES', filetypes):
  1021. # cut off any directory in the original filename
  1022. newfilename = self.make_path(filestorage.filename.split("/")[-1],
  1023. dirc='IMG_DIR', ext=None)
  1024. # verify no name clash
  1025. if not os.path.exists(newfilename):
  1026. os.rename(filename,
  1027. newfilename.encode(sys.getfilesystemencoding()))
  1028. if thumbnail == filename:
  1029. thumbnail = newfilename
  1030. filename = newfilename
  1031. else:
  1032. os.unlink(filename)
  1033. raise WakaError(strings.DUPENAME)
  1034. if self.options['ENABLE_LOAD']:
  1035. # TODO, some day
  1036. raise NotImplementedError('ENABLE_LOAD not implemented')
  1037. # Make file and thumbnail world-readable
  1038. os.chmod(filename, 0644)
  1039. if thumbnail:
  1040. os.chmod(thumbnail, 0644)
  1041. # Clear out the board path name.
  1042. filename = filename.replace(self.path, '').lstrip('/')
  1043. if thumbnail.startswith(self.path):
  1044. thumbnail = thumbnail.replace(self.path, '').lstrip('/')
  1045. else:
  1046. thumbnail = thumbnail.replace(local.environ['DOCUMENT_ROOT'], '')
  1047. return (filename.encode(sys.getfilesystemencoding()), md5, width,
  1048. height, thumbnail, tn_width, tn_height)
  1049. def get_reply_link(self, reply, parent='', abbreviated=False,
  1050. force_http=False):
  1051. if parent:
  1052. return self.make_url(thread=parent, hash=reply, abbr=abbreviated,
  1053. force_http=force_http)
  1054. else:
  1055. return self.make_url(thread=reply, abbr=abbreviated,
  1056. force_http=force_http)
  1057. def expand_url(self, filename, force_http=False):
  1058. # TODO: mark this as deprecated?
  1059. # Is the filename already expanded?
  1060. # The generic regex tests for http://, https://, ftp://, etc.
  1061. if filename.startswith("/") or re.match('\w+:', filename):
  1062. return filename
  1063. if force_http:
  1064. host_url = local.environ['werkzeug.request'].host_url.strip('/')
  1065. self_path = host_url + self.url
  1066. else:
  1067. self_path = self.url
  1068. return os.path.join(self_path, str_format.percent_encode(filename))
  1069. def make_anonymous(self, ip, time):
  1070. # TODO: SILLY_ANONYMOUS not supported
  1071. return self.options['S_ANONAME']
  1072. def make_id_code(self, ip, timestamp, link):
  1073. # TODO not implemented
  1074. raise NotImplementedError()
  1075. def get_post(self, num):
  1076. '''Returns None or WakaPost'''
  1077. session = model.Session()
  1078. sql = self.table.select(self.table.c.num == num)
  1079. row = session.execute(sql).fetchone()
  1080. if row:
  1081. return WakaPost(row)
  1082. def get_parent_post(self, parentid):
  1083. session = model.Session()
  1084. sql = self.table.select(and_(self.table.c.num == parentid,
  1085. self.table.c.parent == 0))
  1086. query = session.execute(sql)
  1087. return WakaPost(query.fetchone())
  1088. def sage_count(self, parent):
  1089. session = model.Session()
  1090. sql = select([func.count()], 'parent=:parent AND '
  1091. 'NOT (timestamp<:timestamp AND ip=:ip)', self.table).params(
  1092. parent=parent.num,
  1093. timestamp=parent.timestamp + self.options['NOSAGE_WINDOW'],
  1094. ip=parent.ip)
  1095. row = session.execute(sql).fetchone()
  1096. return row[0]
  1097. def trim_database(self):
  1098. session = model.Session()
  1099. table = self.table
  1100. max_age = self.options['MAX_AGE']
  1101. # Clear expired posts due to age.
  1102. if max_age:
  1103. mintime = time.time() - max_age * 3600
  1104. sql = table.select().where(and_(table.c.parent == 0,
  1105. table.c.timestamp <= mintime,
  1106. table.c.stickied == 0))
  1107. query = session.execute(sql)
  1108. for row in query:
  1109. self.delete_post(row.num, '', False,
  1110. self.options['ARCHIVE_MODE'], admin=True)
  1111. # TODO: Implement other maxes (even though no one freakin' uses
  1112. # them). :3c
  1113. def toggle_thread_state(self, task_data, num, operation,
  1114. enable_state=True):
  1115. task_data.user.check_access(self.name)
  1116. # Check thread
  1117. session = model.Session()
  1118. table = self.table
  1119. sql = select([table.c.parent], table.c.num == num, table)
  1120. row = session.execute(sql).fetchone()
  1121. if not row:
  1122. raise WakaError('Thread %s,%s not found.' % (self.name, num))
  1123. if row['parent']:
  1124. raise WakaError(strings.NOTATHREAD)
  1125. update = {}
  1126. if operation == 'sticky':
  1127. update = {'stickied' : 1 if enable_state else 0}
  1128. else:
  1129. update = {'locked' : 'yes' if enable_state else ''}
  1130. sql = table.update().where(or_(table.c.num == num,
  1131. table.c.parent == num))\
  1132. .values(**update)
  1133. session.execute(sql)
  1134. self.build_cache()
  1135. task_data.contents.append('/%s/%s' % (self.name, num))
  1136. forward_url = misc.make_script_url(task='mpanel', board=self.name)
  1137. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)
  1138. def flood_check(self, ip, timestamp, comment, file, no_repeat,
  1139. report_check):
  1140. session = model.Session()
  1141. flood_param = self.options['RENZOKU']
  1142. table = self.table
  1143. ip_column = table.c.ip
  1144. err_str = strings.RENZOKU
  1145. if report_check:
  1146. flood_param = config.REPORT_RENZOKU
  1147. table = model.report
  1148. ip_column = table.c.reporter
  1149. elif file:
  1150. # File posts get different flooding rules.
  1151. err_str = strings.RENZOKU2
  1152. flood_param = self.options['RENZOKU2']
  1153. maxtime = time.time() - flood_param
  1154. sql = select([func.count()],
  1155. and_(ip_column == ip, table.c.timestamp > maxtime))
  1156. row = session.execute(sql).fetchone()
  1157. if row[0] != 0:
  1158. raise WakaError(err_str)
  1159. if no_repeat and not report_check and not file:
  1160. # Check for repeated text-only messsages.
  1161. maxtime = time.time() - self.options['RENZOKU3']
  1162. sql = select([func.count()],
  1163. and_(ip_column == ip, table.c.comment == comment,
  1164. timestamp > maxtime))
  1165. row = session.execute(sql).fetchone()
  1166. if row[0] != 0:
  1167. raise WakaError(strings.RENZOKU3)
  1168. def update_rss(self):
  1169. rss_file = os.path.join(self.path, 'board.rss')
  1170. session = model.Session()
  1171. table = self.table
  1172. sql = table.select().order_by(table.c.num.desc())\
  1173. .limit(config.RSS_LENGTH)
  1174. posts = [dict(post.items()) for post in session.execute(sql)]
  1175. for post in posts:
  1176. filename = post['image']
  1177. if filename:
  1178. post['mime_type'] = mimetypes.guess_type(filename)[0]
  1179. Template('rss_template', items=posts,
  1180. pub_date=misc.make_date(time.time(), 'http'))\
  1181. .render_to_file(rss_file)
  1182. def proxy_check(self, ip):
  1183. session = model.Session()
  1184. # TODO proxy_clean
  1185. sql = select([func.count()], 'type="black" AND ip=:ip',
  1186. model.proxy).params(ip=ip)
  1187. row = session.execute(sql).fetchone()
  1188. if row and row[0]:
  1189. raise WakaError(strings.PROXY, plain=True)
  1190. sql = select([func.count()], 'type="white" AND ip=:ip',
  1191. model.proxy).params(ip=ip)
  1192. row = session.execute(sql).fetchone()
  1193. is_white = (row and row[0])
  1194. timestamp = time.time()
  1195. date = misc.make_date(timestamp, self.options['DATE_STYLE'])
  1196. if is_white:
  1197. # known good IP, refresh entry
  1198. sql = model.proxy.update().where(model.proxy.c.ip == ip)\
  1199. .values(timestamp=timestamp, date=date)
  1200. session.execute(sql)
  1201. else:
  1202. # unknown IP, check for proxy
  1203. # enterprise command launching system
  1204. # may send crap to stderr on failure
  1205. retval = os.system(self.options['PROXY_COMMAND'] + " %s" % ip)
  1206. sql = model.proxy.insert().values(ip=ip,
  1207. timestamp=timestamp, date=date)
  1208. retval_blacklist = self.options.get('PROXY_RETVAL_BLACKLIST', 100)
  1209. if retval == retval_blacklist:
  1210. session.execute(sql.values(type='black'))
  1211. raise WakaError(strings.PROXY, plain=True)
  1212. else:
  1213. session.execute(sql.values(type='white'))
  1214. def sticky_lock_check(self, wakapost, admin_mode):
  1215. '''Checks for sticky status (or locked) and updates the whole thread
  1216. if it's possible to post there. Raises exception on locked thread.
  1217. Modifies wakapost if needed.'''
  1218. sticky_check = model.Session().execute(
  1219. select([self.table.c.stickied, self.table.c.locked],
  1220. self.table.c.num == wakapost.parent)).fetchone()
  1221. if sticky_check is None:
  1222. raise WakaError('Thread not found.')
  1223. if sticky_check['stickied']:
  1224. wakapost.stickied = True
  1225. elif not admin_mode:
  1226. wakapost.stickied = False
  1227. if sticky_check['locked'] == 'yes':
  1228. if not admin_mode:
  1229. raise WakaError(strings.THREADLOCKEDERROR)
  1230. else:
  1231. wakapost.locked = True
  1232. return (wakapost.stickied, wakapost.locked)
  1233. def sticky_lock_update(self, parent, stickied, locked):
  1234. '''Update the whole thread to make it all sticky or locked'''
  1235. threadupdate = self.table.update().where(
  1236. or_(self.table.c.num == parent,
  1237. self.table.c.parent == parent))
  1238. do_thread_update = False
  1239. if stickied:
  1240. threadupdate = threadupdate.values(stickied=1)
  1241. do_thread_update = True
  1242. if locked:
  1243. threadupdate = threadupdate.values(locked='yes')
  1244. do_thread_update = True
  1245. if do_thread_update:
  1246. model.Session().execute(threadupdate)
  1247. def update_bump(self, wakapost, parent_res):
  1248. '''Bumping - check for sage, or too many replies'''
  1249. if (wakapost.email.lower() == "mailto:sage" or
  1250. self.sage_count(parent_res) > self.options['MAX_RES']):
  1251. return
  1252. model.Session().execute(self.table.update()
  1253. .where(or_(self.table.c.num == wakapost.parent,
  1254. self.table.c.parent == wakapost.parent))
  1255. .values(lasthit=wakapost.timestamp))
  1256. class NoBoard(object):
  1257. '''Object that provides the minimal attributes to use a few templates
  1258. when no board is defined.'''
  1259. name = ''
  1260. path = ''
  1261. options = {
  1262. 'FAVICON': '',
  1263. 'DEFAULT_STYLE': 'futaba',
  1264. }
  1265. def expand_url(self, url, force_http=False):
  1266. return url
  1267. # utility functions
  1268. def get_page_count(threads, per_page):
  1269. return (len(threads) + per_page - 1) / per_page
  1270. def abbreviate_html(html, max_lines, approx_len):
  1271. lines = chars = 0
  1272. stack = []
  1273. if not max_lines:
  1274. return
  1275. for match in re.finditer("(?:([^<]+)|<(/?)(\w+).*?(/?)>)", html):
  1276. text, closing, tag, implicit = match.groups()
  1277. tag = tag.lower() if tag else ''
  1278. if text:
  1279. chars += len(text)
  1280. else:
  1281. if not closing and not implicit:
  1282. stack.append(tag)
  1283. if closing:
  1284. try:
  1285. stack.pop()
  1286. except IndexError:
  1287. pass
  1288. if (closing or implicit) and (tag in ('p', 'blockquote',
  1289. 'pre', 'li', 'ol', 'ul', 'br')):
  1290. lines += (chars / approx_len) + 1
  1291. if tag in ('p', 'blockquote'):
  1292. lines += 1
  1293. chars = 0
  1294. if lines > max_lines:
  1295. # check if there's anything left other than end-tags
  1296. if re.match("^(?:\s*</\w+>)*\s*$", html[match.end():]):
  1297. return
  1298. abbrev = html[:match.end()]
  1299. while stack:
  1300. tag = stack.pop()
  1301. abbrev += "</%s>" % tag
  1302. return abbrev
  1303. class BoardNotFound(WakaError):
  1304. def __init__(self, message='Board not found'):
  1305. WakaError.__init__(self, message)