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.
 
 
 
 
 
 

635 lines
21 KiB

  1. '''Operations that affect multiple boards or the entire site,
  2. e.g., transferring and merging threads.'''
  3. import time
  4. import re
  5. import os
  6. import sys
  7. import traceback
  8. from datetime import datetime
  9. from calendar import timegm
  10. from subprocess import Popen
  11. import config
  12. import strings
  13. import board
  14. import staff
  15. import model
  16. import util
  17. import str_format
  18. import misc
  19. from template import Template
  20. from util import WakaError, local
  21. from sqlalchemy.sql import or_, and_, select
  22. # Common Site Table!
  23. def get_all_boards(check_board_name=''):
  24. '''Get all the board names. All of them.'''
  25. session = model.Session()
  26. table = model.common
  27. sql = select([table.c.board]).order_by(table.c.board)
  28. query = session.execute(sql)
  29. board_present = False
  30. boards = []
  31. for row in query:
  32. boards.append({'board_entry' : row['board']})
  33. if row['board'] == check_board_name:
  34. board_present = True
  35. if check_board_name and not board_present:
  36. add_board_to_index(check_board_name)
  37. boards.append({'board_entry' : check_board_name})
  38. return boards
  39. def add_board_to_index(board_name):
  40. session = model.Session()
  41. table = model.common
  42. sql = table.insert().values(board=board_name, type='')
  43. session.execute(sql)
  44. def remove_board_from_index(board_name):
  45. session = model.Session()
  46. table = model.common
  47. sql = table.delete().where(table.c.board == board_name)
  48. session.execute(sql)
  49. # Board looping (andwich pattern).
  50. def loop_thru_boards(board_obj_task, exc_msg, *args, **kwargs):
  51. try:
  52. boards = kwargs.pop('boards')
  53. except KeyError:
  54. boards = None
  55. if not boards:
  56. boards = [x['board_entry'] for x in get_all_boards()]
  57. for board_str in boards:
  58. try:
  59. board_obj = board.Board(board_str)
  60. local.environ['waka.board'] = board_obj
  61. getattr(board_obj, board_obj_task)(*args, **kwargs)
  62. board_obj.rebuild_cache()
  63. except:
  64. if exc_msg:
  65. sys.stderr.write(exc_msg % board_str + '\n')
  66. traceback.print_exc(file=sys.stderr)
  67. # Global rebuilding
  68. def global_cache_rebuild():
  69. loop_thru_boards('rebuild_cache', 'Error in global cache rebuild in %s')
  70. def global_cache_rebuild_proxy(task_data):
  71. if task_data.user.account != staff.ADMIN:
  72. raise WakaError(strings.INSUFFICIENTPRIVILEGES)
  73. Popen([sys.executable, sys.argv[0], 'rebuild_global_cache'],
  74. env=util.proxy_environ())
  75. referer = local.environ['HTTP_REFERER']
  76. task_data.contents.append(referer)
  77. return util.make_http_forward(referer, config.ALTERNATE_REDIRECT)
  78. # Global post management.
  79. def process_global_delete_by_ip(ip, boards):
  80. loop_thru_boards(
  81. 'delete_by_ip',
  82. 'Error in deleting posts from %s in %%s' % ip,
  83. task_data = None,
  84. ip = ip,
  85. boards = boards
  86. )
  87. # Bans and Whitelists
  88. def add_admin_entry(task_data, option, comment, ip='', mask='255.255.255.255',
  89. sval1='', total='', expiration=0,
  90. caller=''):
  91. session = model.Session()
  92. table = model.admin
  93. ival1 = ival2 = 0
  94. if not comment:
  95. raise WakaError(strings.COMMENT_A_MUST)
  96. if option in ('ipban', 'whitelist'):
  97. if not ip:
  98. raise WakaError('IP address required.')
  99. if not mask:
  100. mask = '255.255.255.255'
  101. # Convert to decimal.
  102. (ival1, ival2) = (misc.dot_to_dec(ip), misc.dot_to_dec(mask))
  103. sql = table.select().where(table.c.type == option)
  104. query = session.execute(sql)
  105. for row in query:
  106. try:
  107. if int(row.ival1) & int(row.ival2) == ival1 & ival2:
  108. raise WakaError('IP address and mask match ban #%d.' % \
  109. (row.num))
  110. except ValueError:
  111. raise WakaError("Entry #%s on ban table is inconsistent. "
  112. "This shouldn't happen." % row.num)
  113. # Add info to task data.
  114. content = ip + (' (' + mask + ')' if mask else '')
  115. if total == 'yes':
  116. add_htaccess_entry(ip)
  117. content += ' (no browse)'
  118. content += ' "' + comment + '"'
  119. task_data.contents.append(content)
  120. else:
  121. if not sval1:
  122. raise WakaError(strings.STRINGFIELDMISSING)
  123. sql = table.select().where(and_(table.c.sval1 == sval1,
  124. table.c.type == option))
  125. row = session.execute(sql).fetchone()
  126. if row:
  127. raise WakaError('Duplicate String in ban #%d.' % (row.num))
  128. # Add ifno to task data.
  129. task_data.contents.append(sval1)
  130. comment = str_format.clean_string(\
  131. str_format.decode_string(comment, config.CHARSET))
  132. expiration = int(expiration) if expiration else 0
  133. if expiration:
  134. expiration = expiration + time.time()
  135. sql = table.insert().values(type=option, comment=comment, ival1=int(ival1),
  136. ival2=int(ival2), sval1=sval1, total=total,
  137. expiration=expiration)
  138. result = session.execute(sql)
  139. task_data.admin_id = result.inserted_primary_key[0]
  140. # Add specific action name to task data.
  141. task_data.action = option
  142. board = local.environ['waka.board']
  143. forward_url = misc.make_script_url(task='bans', board=board.name)
  144. if caller == 'window':
  145. return Template('edit_successful')
  146. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)
  147. def remove_admin_entry(task_data, num, override_log=False, no_redirect=False):
  148. session = model.Session()
  149. table = model.admin
  150. sql = table.select().where(table.c.num == num)
  151. row = session.execute(sql).fetchone()
  152. if not row:
  153. raise WakaError('Entry not found. Deleted?')
  154. ival1 = row['ival1']
  155. ip = misc.dec_to_dot(ival1) if ival1 else ''
  156. string_val = row['sval1']
  157. if row['total']:
  158. remove_htaccess_entry(ip)
  159. sql = table.delete().where(table.c.num == num)
  160. session.execute(sql)
  161. task_data.action = row['type'] + '_remove'
  162. if string_val:
  163. task_data.contents.append(row['sval1'])
  164. else:
  165. task_data.contents.append(ip + ' (' + misc.dec_to_dot(row['ival2']) \
  166. + ')')
  167. board = local.environ['waka.board']
  168. forward_url = misc.make_script_url(task='bans', board=board.name)
  169. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)
  170. def remove_old_bans():
  171. session = model.Session()
  172. table = model.admin
  173. sql = select([table.c.ival1, table.c.total],
  174. and_(table.c.expiration <= time.time(),
  175. table.c.expiration != 0))
  176. query = session.execute(sql)
  177. for row in query:
  178. sql = table.delete().where(table.c.ival1 == row['ival1'])
  179. session.execute(sql)
  180. if row['total']:
  181. ip = misc.dec_to_dot(row['ival1'])
  182. remove_htaccess_entry(ip)
  183. def remove_old_backups():
  184. session = model.Session()
  185. table = model.backup
  186. sql = table.select().where(table.c.timestampofarchival.op('+')\
  187. (config.POST_BACKUP_EXPIRE) <= time.time())
  188. query = session.execute(sql)
  189. for row in query:
  190. board_obj = board.Board(row['board_name'])
  191. backup_path = os.path.join(board_obj.path,
  192. board_obj.options['ARCHIVE_DIR'],
  193. board_obj.options['BACKUP_DIR'], '')
  194. if row.image:
  195. # Delete backup image; then, mark post for deletion.
  196. filename = os.path.join(backup_path, os.path.basename(row.image))
  197. if os.path.exists(filename):
  198. os.unlink(filename)
  199. if row.thumbnail \
  200. and re.match(board_obj.options['THUMB_DIR'], row.thumbnail):
  201. filename = os.path.join(backup_path,
  202. os.path.basename(row.thumbnail))
  203. if os.path.exists(filename):
  204. os.unlink(filename)
  205. # Perform SQL DELETE
  206. sql = table.delete().where(table.c.timestampofarchival.op('+')\
  207. (config.POST_BACKUP_EXPIRE) <= time.time())
  208. session.execute(sql)
  209. def add_htaccess_entry(ip):
  210. htaccess = os.path.join(local.environ['DOCUMENT_ROOT'],
  211. config.HTACCESS_PATH, '.htaccess')
  212. with util.FileLock(htaccess):
  213. with open(htaccess, 'r') as f:
  214. ban_entries_found = False
  215. line = f.readline()
  216. while line:
  217. if line.count('RewriteEngine On'):
  218. ban_entries_found = True
  219. break
  220. line = f.readline()
  221. with open(htaccess, 'a') as f:
  222. if not ban_entries_found:
  223. f.write("\n"+'# Bans added by Wakarimasen'+"\n")
  224. f.write("\n"+'RewriteEngine On'+"\n")
  225. ip = ip.replace('.', r'\.')
  226. f.write('RewriteCond %{REMOTE_ADDR} ^'+ip+'$'+"\n")
  227. f.write('RewriteRule !(\+pl|\+js$|\+css$|\+png'\
  228. '|ban_images) '+local.environ['SCRIPT_NAME']+'?'\
  229. 'task=banreport&board='\
  230. +local.environ['waka.board'].name+"\n")
  231. def remove_htaccess_entry(ip):
  232. ip = ip.replace('.', r'\.')
  233. htaccess = os.path.join(local.environ['DOCUMENT_ROOT'],
  234. config.HTACCESS_PATH, '.htaccess')
  235. with util.FileLock(htaccess):
  236. lines = []
  237. with open(htaccess, 'r') as f:
  238. line = f.readline()
  239. while line:
  240. if not line.count('RewriteCond %{REMOTE_ADDR} ^' + ip + '$'):
  241. lines.append(line)
  242. else:
  243. # Do not write, and skip the next line.
  244. line = f.readline()
  245. if line:
  246. line = f.readline()
  247. with open(htaccess, 'w') as f:
  248. f.write(''.join(lines))
  249. def ban_check(numip, name, subject, comment):
  250. '''This function raises an exception if the IP address is banned, or
  251. the post contains a forbidden (non-spam) string. It otherwise returns
  252. nothing.'''
  253. session = model.Session()
  254. table = model.admin
  255. # IP Banned?
  256. sql = table.select().where(and_(table.c.type == 'ipban',
  257. table.c.ival1.op('&')(table.c.ival2) \
  258. == table.c.ival2.op('&')(numip)))
  259. ip_row = session.execute(sql).fetchone()
  260. if ip_row:
  261. raise WakaError('Address %s banned. Reason: %s' % \
  262. (misc.dec_to_dot(numip), ip_row.comment))
  263. # To determine possible string bans, first normalize input to lowercase.
  264. comment = comment.lower()
  265. subject = subject.lower()
  266. name = name.lower()
  267. sql = select([table.c.sval1], table.c.type == 'wordban')
  268. query = session.execute(sql)
  269. for row in query:
  270. bad_string = row.sval1.lower()
  271. if comment.count(bad_string) or subject.count(bad_string) or \
  272. name.count(bad_string):
  273. raise WakaError(strings.STRREF)
  274. def mark_resolved(task_data, delete, posts):
  275. referer = local.environ['HTTP_REFERER']
  276. user = task_data.user
  277. errors = []
  278. board_obj = None
  279. old_board_obj = local.environ['waka.board']
  280. for (board_name, posts) in posts.iteritems():
  281. # Access rights enforcement.
  282. if user.account == staff.MODERATOR and board_name not in user.reign:
  283. errors.append({'error' : '/%s/*: Sorry, you lack access rights.'\
  284. % (board_name)})
  285. continue
  286. for post in posts:
  287. session = model.Session()
  288. table = model.report
  289. sql = table.select().where(and_(table.c.postnum == post,
  290. table.c.board == board_name))
  291. row = session.execute(sql).fetchone()
  292. if not row:
  293. errors.append({'error' : '%s,%d: Report not found.'\
  294. % (board_name, int(post))})
  295. continue
  296. sql = table.delete().where(and_(table.c.postnum == post,
  297. table.c.board == board_name))
  298. session.execute(sql)
  299. # Log the resolved post.
  300. task_data.contents.append('/'.join(['', board_name, post]))
  301. if delete:
  302. try:
  303. board_obj = board.Board(board_name)
  304. local.environ['waka.board'] = board_obj
  305. except WakaError:
  306. errors.append({'error' : '%s,*: Error loading board.'\
  307. % (board_name)})
  308. continue
  309. try:
  310. board_obj.delete_stuff(posts, '', False, False,
  311. admindelete=True,
  312. admin_data=task_data)
  313. except WakaError:
  314. errors.append({'error' : '%s,%d: Post already deleted.'\
  315. % (board_name, int(post))})
  316. local.environ['waka.board'] = old_board_obj
  317. # TODO: This probably should be refactored into StaffInterface.
  318. return Template('report_resolved', errors=errors,
  319. error_occurred=len(errors)>0,
  320. admin=user.login_data.cookie,
  321. username=user.username,
  322. type=user.account,
  323. boards_select=user.reign,
  324. referer=referer)
  325. def edit_admin_entry(task_data, num, comment='', ival1=None,
  326. ival2='255.255.255.255', sval1='', total=False,
  327. sec=None, min=None, hour=None, day=None, month=None,
  328. year=None, noexpire=False):
  329. session = model.Session()
  330. table = model.admin
  331. sql = table.select().where(table.c.num == num)
  332. row = session.execute(sql).fetchone()
  333. if not row:
  334. raise WakaError('Entry was not created or was removed.')
  335. task_data.action = row.type + '_edit'
  336. if row.type in ('ipban', 'whitelist'):
  337. if not noexpire:
  338. try:
  339. expiration = datetime(int(year), int(month), int(day),
  340. int(hour), int(min), int(sec))
  341. except:
  342. raise WakaError('Invalid date.')
  343. expiration = timegm(expiration.utctimetuple())
  344. else:
  345. expiration = 0
  346. ival1 = misc.dot_to_dec(ival1)
  347. ival2 = misc.dot_to_dec(ival2)
  348. task_data.contents.append(ival1 + ' (' + ival2 + ')')
  349. else:
  350. expiration = 0
  351. task_data.contents.append(sval1)
  352. sql = table.update().where(table.c.num == num)\
  353. .values(comment=comment, ival1=ival1, ival2=ival2, sval1=sval1,
  354. total=total, expiration=expiration)
  355. row = session.execute(sql)
  356. return Template('edit_successful')
  357. def delete_by_ip(task_data, ip, mask='255.255.255.255', caller=''):
  358. task_data.contents.append(ip)
  359. user = task_data.user
  360. if user.account == staff.MODERATOR:
  361. reign = user.reign
  362. else:
  363. reign = [x['board_entry'] for x in get_all_boards()]
  364. Popen([sys.executable, sys.argv[0], 'delete_by_ip', ip, ','.join(reign)],
  365. env=util.proxy_environ())
  366. board_name = local.environ['waka.board'].name
  367. redir = misc.make_script_url(task='mpanel', board=board_name)
  368. if caller != 'internal':
  369. return util.make_http_forward(redir, config.ALTERNATE_REDIRECT)
  370. def trim_reported_posts(date=0):
  371. mintime = 0
  372. if date:
  373. mintime = time.time() - date
  374. elif config.REPORT_RETENTION:
  375. mintime = time.time() - config.REPORT_RETENTION
  376. if mintime > 0:
  377. session = model.Session()
  378. table = model.report
  379. sql = table.delete().where(table.c.timestamp <= mintime)
  380. session.execute(sql)
  381. def trim_activity():
  382. mintime = time.time() - config.STAFF_LOG_RETENTION
  383. session = model.Session()
  384. table = model.activity
  385. sql = table.delete().where(table.c.timestamp <= mintime)
  386. session.execute(sql)
  387. def update_spam_file(task_data, spam):
  388. if task_data.user.account == staff.MODERATOR:
  389. raise WakaError(strings.INSUFFICIENTPRIVILEGES)
  390. # Dump all contents to first spam file.
  391. with open(config.SPAM_FILES[0], 'w') as f:
  392. f.write(spam)
  393. board = local.environ['waka.board']
  394. forward_url = misc.make_script_url(task='spam', board=board.name)
  395. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)
  396. # Thread Transfer
  397. def move_thread(task_data, parent, src_brd_obj, dest_brd_obj):
  398. if not parent:
  399. raise WakaError('No thread specified.')
  400. if src_brd_obj.name == dest_brd_obj.name:
  401. raise WakaError('Source and destination boards match.')
  402. # Check administrative access rights to both boards.
  403. user = task_data.user
  404. user.check_access(src_brd_obj.name)
  405. user.check_access(dest_brd_obj.name)
  406. session = model.Session()
  407. src_table = src_brd_obj.table
  408. dest_table = dest_brd_obj.table
  409. sql = select([src_table.c.parent], src_table.c.num == parent)
  410. row = session.execute(sql).fetchone()
  411. if not row:
  412. raise WakaError('Thread not found.')
  413. elif row[0]:
  414. # Automatically correct if reply instead of thread was given.
  415. parent = row[0]
  416. sql = src_table.select().where(or_(src_table.c.num == parent,
  417. src_table.c.parent == parent))\
  418. .order_by(src_table.c.num.asc())
  419. thread = [dict(x.items()) for x in session.execute(sql).fetchall()]
  420. # Indicate OP post number after insertion.
  421. new_parent = 0
  422. # List of images/thumbs to move around.
  423. image_move = []
  424. thumb_move = []
  425. lasthit = time.time()
  426. # DB operations
  427. for post in thread:
  428. # Grab post contents as dictionary of updates. Remove primary key.
  429. del post['num']
  430. post['lasthit'] = lasthit
  431. image = post['image']
  432. thumbnail = post['thumbnail']
  433. if image:
  434. image_move.append(image)
  435. if re.match(src_brd_obj.options['THUMB_DIR'], thumbnail):
  436. thumb_move.append(thumbnail)
  437. # Update post reference links.
  438. if new_parent:
  439. post['parent'] = new_parent
  440. new_comment = re.sub(r'a href="(.*?)'
  441. + os.path.join(src_brd_obj.path,
  442. src_brd_obj.options['RES_DIR'],
  443. '%d%s' % (int(parent)), config.PAGE_EXT),
  444. r'a href="\1' + os.path.join(\
  445. dest_brd_obj.path,
  446. dest_brd_obj.options['RES_DIR'],
  447. '%d%s' % (int((new_parent), config.PAGE_EXT))),
  448. post['comment'])
  449. post['comment'] = new_comment
  450. sql = dest_table.insert().values(**post)
  451. result = session.execute(sql)
  452. if not new_parent:
  453. new_parent = result.inserted_primary_key[0]
  454. # Nested associate for moving files in bulk.
  455. def rename_files(filename_list, dir_type):
  456. for filename in filename_list:
  457. src_filename = os.path.join(src_brd_obj.path, filename)
  458. dest_filename = re.sub('^/?' + src_brd_obj.options[dir_type],
  459. dest_brd_obj.options[dir_type],
  460. filename)
  461. dest_filename = os.path.join(dest_brd_obj.path, dest_filename)
  462. os.rename(src_filename, dest_filename)
  463. # File transfer operations.
  464. rename_files(image_move, 'IMG_DIR')
  465. rename_files(thumb_move, 'THUMB_DIR')
  466. dest_brd_obj.build_cache()
  467. dest_brd_obj.build_thread_cache(new_parent)
  468. src_brd_obj.delete_stuff([parent], '', False, False, caller='internal')
  469. forward_url = misc.make_script_url(task='mpanel',
  470. board=dest_brd_obj.name, page=('t%s' % new_parent))
  471. # Log.
  472. task_data.contents.append('/%s/%d to /%s/%d' \
  473. % (src_brd_obj.name, int(parent),
  474. dest_brd_obj.name, int(new_parent)))
  475. return util.make_http_forward(forward_url)
  476. # proxy
  477. def add_proxy_entry(task_data, type, ip, timestamp):
  478. session = model.Session()
  479. table = model.proxy
  480. if not misc.validate_ip(ip):
  481. raise WakaError(strings.BADIP)
  482. age = config.PROXY_WHITE_AGE if type == 'white' else config.PROXY_BLACK_AGE
  483. timestamp = int(timestamp or '0') - age + time.time()
  484. date = misc.make_date(time.time(), style=config.DATE_STYLE)
  485. query = table.delete().where(table.c.ip == ip)
  486. session.execute(query)
  487. query = table.insert().values(
  488. type=type,
  489. ip=ip,
  490. timestamp=timestamp,
  491. date=date
  492. )
  493. session.execute(query)
  494. board = local.environ['waka.board']
  495. forward_url = misc.make_script_url(task='proxy', board=board.name)
  496. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)
  497. def remove_proxy_entry(task_data, num):
  498. session = model.Session()
  499. table = model.proxy
  500. query = table.delete().where(table.c.num == num)
  501. session.execute(query)
  502. board = local.environ['waka.board']
  503. forward_url = misc.make_script_url(task='proxy', board=board.name)
  504. return util.make_http_forward(forward_url, config.ALTERNATE_REDIRECT)