Browse Source

Merge pull request #114 from Spuds/drafts

Inital commit of post & pm drafts function
emanuele45 12 years ago
parent
commit
e951e60e6c
47 changed files with 2380 additions and 271 deletions
  1. 8 0
      Sources/Admin.php
  2. 13 10
      Sources/Class-BrowserDetect.php
  3. 7 1
      Sources/Display.php
  4. 884 0
      Sources/Drafts.php
  5. 50 38
      Sources/Load.php
  6. 45 11
      Sources/ManageMaintenance.php
  7. 6 0
      Sources/ManagePermissions.php
  8. 25 0
      Sources/ManageSettings.php
  9. 68 12
      Sources/PersonalMessage.php
  10. 38 3
      Sources/Post.php
  11. 11 1
      Sources/Profile.php
  12. 41 1
      Sources/ScheduledTasks.php
  13. 14 16
      Sources/Subs-Editor.php
  14. 9 0
      Sources/Subs-Members.php
  15. 8 5
      Sources/Subs-Post.php
  16. 23 31
      Sources/Subs.php
  17. 0 4
      Themes/default/Admin.template.php
  18. 3 3
      Themes/default/BoardIndex.template.php
  19. 24 2
      Themes/default/Display.template.php
  20. 60 2
      Themes/default/GenericControls.template.php
  21. 18 3
      Themes/default/ManageMaintenance.template.php
  22. 121 1
      Themes/default/PersonalMessage.template.php
  23. 66 5
      Themes/default/Post.template.php
  24. 111 11
      Themes/default/Profile.template.php
  25. BIN
      Themes/default/images/admin/feature_dr.png
  26. 7 0
      Themes/default/languages/Admin.english.php
  27. 41 0
      Themes/default/languages/Drafts.english.php
  28. 2 0
      Themes/default/languages/Errors.english.php
  29. 3 0
      Themes/default/languages/ManageMaintenance.english.php
  30. 17 0
      Themes/default/languages/ManagePermissions.english.php
  31. 3 0
      Themes/default/languages/ManageScheduledTasks.english.php
  32. 2 0
      Themes/default/languages/ManageSettings.english.php
  33. 12 10
      Themes/default/languages/Post.english.php
  34. 2 0
      Themes/default/languages/Reports.english.php
  35. 7 5
      Themes/default/languages/index.english.php
  36. 18 3
      Themes/default/scripts/admin.js
  37. 186 0
      Themes/default/scripts/drafts.js
  38. 20 76
      Themes/default/scripts/script.js
  39. 16 10
      Themes/default/scripts/smf_jquery_plugins.js
  40. 1 1
      Themes/default/scripts/theme.js
  41. 1 5
      Themes/default/scripts/topic.js
  42. 51 1
      other/install_2-1_mysql.sql
  43. 56 0
      other/install_2-1_postgresql.sql
  44. 55 0
      other/install_2-1_sqlite.sql
  45. 77 0
      other/upgrade_2-1_mysql.sql
  46. 75 0
      other/upgrade_2-1_postgresql.sql
  47. 75 0
      other/upgrade_2-1_sqlite.sql

+ 8 - 0
Sources/Admin.php

@@ -227,6 +227,14 @@ function AdminMain()
 						'topics' => array($txt['manageposts_topic_settings']),
 					),
 				),
+				'managedrafts' => array(
+					'label' => $txt['manage_drafts'],
+					'file' => 'Drafts.php',
+					'function' => 'ModifyDraftSettings',
+					'icon' => 'logs.png',
+					'permission' => array('admin_forum'),
+					'enabled' => in_array('dr', $context['admin_features']),
+				),
 				'managecalendar' => array(
 					'label' => $txt['manage_calendar'],
 					'file' => 'ManageCalendar.php',

+ 13 - 10
Sources/Class-BrowserDetect.php

@@ -6,13 +6,13 @@
  * - Detects the following browsers
  * - Opera, Webkit, Firefox, Web_tv, Konqueror, IE, Gecko
  * - Webkit variants: Chrome, iphone, blackberry, android, safari, ipad, ipod
- * - Opera Versions: 6, 7, 8, 9, 10 and mobile mini and mobi
+ * - Opera Versions: 6, 7, 8 ... 10 ... and mobile mini and mobi
  * - Firefox Versions: 1, 2, 3 .... 11 ...
  * - Chrome Versions: 1 ... 18 ...
- * - IE Versions: 4, 5, 5.5, 6, 7, 8, 9, 10 mobile and Mac
- * - Nokia 
+ * - IE Versions: 4, 5, 5.5, 6, 7, 8, 9, 10 ... mobile and Mac
+ * - Nokia
  */
- 
+
 if (!defined('SMF'))
 	die('Hacking attempt...');
 
@@ -53,7 +53,7 @@ class browser_detector
 		// Old friend, old frenemy
 		elseif ($this->isIe())
 			$this->setupIe();
-		
+
 		// Just a few mobile checks
 		$this->isOperaMini();
 		$this->isOperaMobi();
@@ -122,7 +122,7 @@ class browser_detector
 	function isFirefox()
 	{
 		if (!isset($this->_browsers['is_firefox']))
-			$this->_browsers['is_firefox'] = preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/~', $_SERVER['HTTP_USER_AGENT']) === 1;
+			$this->_browsers['is_firefox'] = preg_match('~(?:Firefox|Ice[wW]easel|IceCat|Shiretoko|Minefield)/~', $_SERVER['HTTP_USER_AGENT']) === 1 && $this->isGecko();
 		return $this->_browsers['is_firefox'];
 	}
 
@@ -319,18 +319,21 @@ class browser_detector
 			$context['browser_body_id'] = 'mobile';
 		else
 		{
+			// add in any specific detection conversions here if you want a special body id e.g. 'is_opera9' => 'opera9'
 			$browser_priority = array(
 				'is_ie6' => 'ie6',
 				'is_ie7' => 'ie7',
+				'is_ie8' => 'ie8',
+				'is_ie9' => 'ie9',
+				'is_ie10' => 'ie10',
 				'is_ie' => 'ie',
-				'is_firefox3' => 'firefox3',
-				'is_firefox4' => 'firefox4',
 				'is_firefox' => 'firefox',
 				'is_chrome' => 'chrome',
 				'is_safari' => 'safari',
-				'is_opera8' => 'opera8',
-				'is_opera9' => 'opera9',
 				'is_opera10' => 'opera10',
+				'is_opera11' => 'opera11',
+				'is_opera12' => 'opera12',
+				'is_opera' => 'opera',
 				'is_konqueror' => 'konqueror',
 			);
 

+ 7 - 1
Sources/Display.php

@@ -1079,7 +1079,13 @@ function Display()
 	// Can restore topic?  That's if the topic is in the recycle board and has a previous restore state.
 	$context['can_restore_topic'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_board']);
 	$context['can_restore_msg'] &= !empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] == $board && !empty($topicinfo['id_previous_topic']);
-
+	
+	// Check if the draft functions are enabled and that they have permission to use them (for quick reply.)
+	$context['drafts_save'] = !empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_post_enabled']) && allowedTo('post_draft') && $context['can_reply'];
+	$context['drafts_autosave'] = !empty($context['drafts_save']) && !empty($modSettings['drafts_autosave_enabled']) && allowedTo('post_autosave_draft');
+	if (!empty($context['drafts_save']))
+		loadLanguage('Drafts');
+	
 	// Wireless shows a "more" if you can do anything special.
 	if (WIRELESS && WIRELESS_PROTOCOL != 'wap')
 	{

+ 884 - 0
Sources/Drafts.php

@@ -0,0 +1,884 @@
+<?php
+
+/**
+ * This file contains all the functions that allow for the saving,
+ * retrieving, deleting and settings for the drafts function.
+ *
+ * Simple Machines Forum (SMF)
+ *
+ * @package SMF
+ * @author Simple Machines http://www.simplemachines.org
+ * @copyright 2011 Simple Machines
+ * @license http://www.simplemachines.org/about/smf/license.php BSD
+ *
+ * @version 2.1 Alpha 1
+ */
+
+if (!defined('SMF'))
+	die('Hacking attempt...');
+
+loadLanguage('Drafts');
+
+/**
+ * Saves a post draft in the user_drafts table
+ * The core draft feature must be enabled, as well as the post draft option
+ * Determines if this is a new or an existing draft
+ *
+ * @return boolean
+ */
+function SaveDraft(&$post_errors)
+{
+	global $txt, $context, $user_info, $smcFunc, $modSettings, $board;
+
+	// can you be, should you be ... here?
+	if (empty($modSettings['drafts_enabled']) || empty($modSettings['drafts_post_enabled']) || !allowedTo('post_draft') || !isset($_POST['save_draft']) || !isset($_POST['id_draft']))
+		return false;
+		
+	// read in what they sent us, if anything
+	$id_draft = (int) $_POST['id_draft'];
+	$draft_info = ReadDraft($id_draft);
+
+	// prepare any data from the form
+	$topic_id = empty($_REQUEST['topic']) ? 0 : (int) $_REQUEST['topic'];
+	$draft['icon'] = empty($_POST['icon']) ? 'xx' : preg_replace('~[\./\\\\*:"\'<>]~', '', $_POST['icon']);
+	$draft['smileys_enabled'] = isset($_POST['ns']) ? (int) $_POST['ns'] : 0;
+	$draft['locked'] = isset($_POST['lock']) ? (int) $_POST['lock'] : 0;
+	$draft['sticky'] = isset($_POST['sticky']) && !empty($modSettings['enableStickyTopics']) ? (int) $_POST['sticky'] : 0;
+	$draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => ''));
+	$draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES);
+
+	// message and subject still need a bit more work
+	preparsecode($draft['body']);
+	if ($smcFunc['strlen']($draft['subject']) > 100)
+		$draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100);
+
+	// Modifying an existing draft, like hitting the save draft button or autosave enabled?
+	if (!empty($id_draft) && !empty($draft_info) && $draft_info['id_member'] == $user_info['id'])
+	{
+		$smcFunc['db_query']('', '
+			UPDATE {db_prefix}user_drafts
+			SET
+				id_topic = {int:id_topic},
+				id_board = {int:id_board},
+				poster_time = {int:poster_time},
+				subject = {string:subject},
+				smileys_enabled = {int:smileys_enabled},
+				body = {string:body},
+				icon = {string:icon},
+				locked = {int:locked},
+				is_sticky = {int:is_sticky}
+			WHERE id_draft = {int:id_draft}',
+			array (
+				'id_topic' => $topic_id,
+				'id_board' => $board,
+				'poster_time' => time(),
+				'subject' => $draft['subject'],
+				'smileys_enabled' => (int) $draft['smileys_enabled'],
+				'body' => $draft['body'],
+				'icon' => $draft['icon'],
+				'locked' => $draft['locked'],
+				'is_sticky' => $draft['sticky'],
+				'id_draft' => $id_draft,
+			)
+		);
+
+		// some items to return to the form
+		$context['draft_saved'] = true;
+		$context['id_draft'] = $id_draft;
+		
+		// cleanup
+		unset($_POST['save_draft']);
+	}
+	// otherwise creating a new draft
+	else
+	{
+		$smcFunc['db_insert']('',
+			'{db_prefix}user_drafts',
+			array(
+				'id_topic' => 'int',
+				'id_board' => 'int',
+				'type' => 'int',
+				'poster_time' => 'int',
+				'id_member' => 'int',
+				'subject' => 'string-255',
+				'smileys_enabled' => 'int',
+				'body' => (!empty($modSettings['max_messageLength']) && $modSettings['max_messageLength'] > 65534 ? 'string-' . $modSettings['max_messageLength'] : 'string-65534'),
+				'icon' => 'string-16',
+				'locked' => 'int',
+				'is_sticky' => 'int'
+			),
+			array(
+				$topic_id,
+				$board,
+				0,
+				time(),
+				$user_info['id'],
+				$draft['subject'],
+				$draft['smileys_enabled'],
+				$draft['body'],
+				$draft['icon'],
+				$draft['locked'],
+				$draft['sticky']
+			),
+			array(
+				'id_draft'
+			)
+		);
+
+		// get the id of the new draft
+		$id_draft = $smcFunc['db_insert_id']('{db_prefix}user_drafts', 'id_draft');
+
+		// everything go as expected?
+		if (!empty($id_draft))
+		{
+			$context['draft_saved'] = true;
+			$context['id_draft'] = $id_draft;
+		}
+		else
+			$post_errors[] = 'draft_not_saved';
+			
+		// cleanup
+		unset($_POST['save_draft']);
+	}
+
+	// if we were called from the autosave function, send something back
+	if (!empty($id_draft) && isset($_REQUEST['xml']) && (!in_array('session_timeout', $post_errors)))
+		XmlDraft($id_draft);
+
+	return true;
+}
+
+/**
+ * Saves a PM draft in the user_drafts table
+ * The core draft feature must be enable, as well as the pm draft option
+ * Determines if this is a new or and update to an existing draft
+ *
+ * @global type $context
+ * @global type $user_info
+ * @global type $smcFunc
+ * @global type $modSettings
+ * @param string $post_errors
+ * @param type $recipientList
+ * @return boolean
+ */
+function SavePMDraft(&$post_errors, $recipientList)
+{
+	global $context, $user_info, $smcFunc, $modSettings;
+
+	// PM survey says ... can you stay or must you go
+	if (empty($modSettings['drafts_enabled']) || empty($modSettings['drafts_pm_enabled']) || !allowedTo('pm_draft') || !isset($_POST['save_draft']))
+		return false;
+		
+	// read in what you sent us
+	$id_pm_draft = (int) $_POST['id_pm_draft'];
+	$draft_info = ReadDraft($id_pm_draft, 1);
+
+	// determine who this is being sent to
+	if (isset($_REQUEST['xml']))
+	{
+		$recipientList['to'] = isset($_POST['recipient_to']) ? explode(',', $_POST['recipient_to']) : array();
+		$recipientList['bcc'] = isset($_POST['recipient_bcc']) ? explode(',', $_POST['recipient_bcc']) : array();
+	}
+	elseif (!empty($draft_info['to_list']) && empty($recipientList))
+		$recipientList = unserialize($draft_info['to_list']);
+
+	// prepare the data we got from the form
+	$reply_id = empty($_POST['replied_to']) ? 0 : (int) $_POST['replied_to'];
+	$outbox = empty($_POST['outbox']) ? 0 : 1;
+	$draft['body'] = $smcFunc['htmlspecialchars']($_POST['message'], ENT_QUOTES);
+	$draft['subject'] = strtr($smcFunc['htmlspecialchars']($_POST['subject']), array("\r" => '', "\n" => '', "\t" => ''));
+
+	// message and subject still need a bit more massaging
+	preparsecode($draft['body']);
+	if ($smcFunc['strlen']($draft['subject']) > 100)
+		$draft['subject'] = $smcFunc['substr']($draft['subject'], 0, 100);
+
+	// Modifying an existing PM draft?
+	if (!empty($id_pm_draft) && !empty($draft_info) && $draft_info['id_member'] == $user_info['id'])
+	{
+		$smcFunc['db_query']('', '
+			UPDATE {db_prefix}user_drafts
+			SET id_reply = {int:id_reply},
+				type = {int:type},
+				poster_time = {int:poster_time},
+				subject = {string:subject},
+				body = {string:body},
+				to_list = {string:to_list},
+				outbox = {int:outbox}
+			WHERE id_draft = {int:id_pm_draft}
+			LIMIT 1',
+			array(
+				'id_reply' => $reply_id,
+				'type' => 1,
+				'poster_time' =>  time(),
+				'subject' =>  $draft['subject'],
+				'body' => $draft['body'],
+				'id_pm_draft' => $id_pm_draft,
+				'to_list' => serialize($recipientList),
+				'outbox' => $outbox,
+			)
+		);
+
+		// some items to return to the form
+		$context['draft_saved'] = true;
+		$context['id_pm_draft'] = $id_pm_draft;
+	}
+	// otherwise creating a new PM draft.
+	else
+	{
+		$smcFunc['db_insert']('',
+			'{db_prefix}user_drafts',
+			array(
+				'id_reply' => 'int',
+				'type' => 'int',
+				'poster_time' => 'int',
+				'id_member' => 'int',
+				'subject' => 'string-255',
+				'body' => 'string-65534',
+				'to_list' => 'string-255',
+				'outbox' => 'int',
+			),
+			array(
+				$reply_id,
+				1,
+				time(),
+				$user_info['id'],
+				$draft['subject'],
+				$draft['body'],
+				serialize($recipientList),
+				$outbox,
+			),
+			array(
+				'id_draft'
+			)
+		);
+
+		// get the new id
+		$id_pm_draft = $smcFunc['db_insert_id']('{db_prefix}user_drafts', 'id_draft');
+
+		// everything go as expected, if not toss an error
+		if (!empty($id_pm_draft))
+		{
+			$context['draft_saved'] = true;
+			$context['id_pm_draft'] = $id_pm_draft;
+		}
+		else
+			$post_errors[] = 'draft_not_saved';
+	}
+
+	// if we were called from the autosave function, send something back
+	if (!empty($id_pm_draft) && isset($_REQUEST['xml']) && !in_array('session_timeout', $post_errors))
+		XmlDraft($id_pm_draft);
+
+	return;
+}
+
+/**
+ * Reads a draft in from the user_drafts table
+ * Only loads the draft of a given type 0 for post, 1 for pm draft
+ * validates that the draft is the users draft
+ * Optionally loads the draft in to context or superglobal for loading in to the form
+ *
+ * @param type $id_draft - draft to load
+ * @param type $type - type of draft
+ * @param type $check - validate the user
+ * @param type $load - load it for use in a form
+ * @return boolean
+ */
+function ReadDraft($id_draft, $type = 0, $check = true, $load = false)
+{
+	global $context, $user_info, $smcFunc, $modSettings;
+
+	// always clean to be sure
+	$id_draft = (int) $id_draft;
+	$type = (int) $type;
+
+	// nothing to read, nothing to do
+	if (empty($id_draft))
+		return false;
+
+	// load in this draft from the DB
+	$request = $smcFunc['db_query']('', '
+		SELECT *
+		FROM {db_prefix}user_drafts
+		WHERE id_draft = {int:id_draft}' . ($check ? '
+			AND id_member = {int:id_member}' : '') . '
+			AND type = {int:type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : '') . '
+		LIMIT 1',
+		array(
+			'id_member' => $user_info['id'],
+			'id_draft' => $id_draft,
+			'type' => $type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+
+	// no results?
+	if (!$smcFunc['db_num_rows']($request))
+		return false;
+
+	// load up the data
+	$draft_info = $smcFunc['db_fetch_assoc']($request);
+	$smcFunc['db_free_result']($request);
+
+	// Load it up for the templates as well
+	$recipients = array();
+	if (!empty($load))
+	{
+		if ($type === 0)
+		{
+			// a standard post draft?
+			$context['sticky'] = !empty($draft_info['is_sticky']) ? $draft_info['is_sticky'] : '';
+			$context['locked'] = !empty($draft_info['locked']) ? $draft_info['locked'] : '';
+			$context['use_smileys'] = !empty($draft_info['smileys_enabled']) ? true : false;
+			$context['icon'] = !empty($draft_info['icon']) ? $draft_info['icon'] : 'xx';
+			$context['message'] = !empty($draft_info['body']) ? str_replace('<br />', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : '';
+			$context['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : '';
+			$context['board'] = !empty($draft_info['board_id']) ? $draft_info['id_board'] : '';
+			$context['id_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0;
+		}
+		elseif ($type === 1)
+		{
+			// one of those pm drafts? then set it up like we have an error
+			$_REQUEST['outbox'] = !empty($draft_info['outbox']);
+			$_REQUEST['subject'] = !empty($draft_info['subject']) ? stripslashes($draft_info['subject']) : '';
+			$_REQUEST['message'] = !empty($draft_info['body']) ? str_replace('<br />', "\n", un_htmlspecialchars(stripslashes($draft_info['body']))) : '';
+			$_REQUEST['replied_to'] = !empty($draft_info['id_reply']) ? $draft_info['id_reply'] : 0;
+			$context['id_pm_draft'] = !empty($draft_info['id_draft']) ? $draft_info['id_draft'] : 0;
+			$recipients = unserialize($draft_info['to_list']);
+
+			// make sure we only have integers in this array
+			$recipients['to'] = array_map('intval', $recipients['to']);
+			$recipients['bcc'] = array_map('intval', $recipients['bcc']);
+
+			// pretend we messed up to populate the pm message form
+			messagePostError(array(), array(), $recipients);
+			return true;
+		}
+	}
+
+	return $draft_info;
+}
+
+/**
+ * Deletes one or many drafts from the DB
+ * Validates the drafts are from the user
+ * is supplied an array of drafts will attempt to remove all of them
+ *
+ * @param type $id_draft
+ * @param type $check
+ * @return boolean
+ */
+function DeleteDraft($id_draft, $check = true)
+{
+	global $user_info, $smcFunc;
+
+	// Only a single draft.
+	if (is_numeric($id_draft))
+		$id_draft = array($id_draft);
+
+	// can't delete nothing
+	if (empty($id_draft) || ($check && empty($user_info['id'])))
+		return false;
+
+	$smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}user_drafts
+		WHERE draft_id IN ({array_int:draft_id})', ($check ? '
+			AND  id_member = {int:id_member}' : ''), '
+		LIMIT 1',
+		array (
+			'draft_id' => $id_draft,
+			'id_member' => empty($user_info['id']) ? -1 : $user_info['id'],
+		)
+	);
+}
+
+/**
+ * Loads in a group of drafts for the user of a given type (0/posts, 1/pm's)
+ * loads a specific draft for forum use if selected.
+ * Used in the posting screens to allow draft selection
+ * WIll load a draft if selected is supplied via post
+ *
+ * @param type $member_id
+ * @param type $topic
+ * @param type $draft_type
+ * @return boolean
+ */
+function ShowDrafts($member_id, $topic = false, $draft_type = 0)
+{
+	global $smcFunc, $scripturl, $context, $txt, $modSettings;
+
+	// Permissions
+	if (($draft_type === 0 && empty($context['drafts_save'])) || ($draft_type === 1 && empty($context['drafts_pm_save'])) || empty($member_id))
+		return false;
+
+	$context['drafts'] = array();
+
+	// has a specific draft has been selected?  Load it up if there is not a message already in the editor
+	if (isset($_REQUEST['id_draft']) && empty($_POST['subject']) && empty($_POST['message']))
+		ReadDraft((int) $_REQUEST['id_draft'], $draft_type, true, true);
+
+	// load the drafts this user has available
+	$request = $smcFunc['db_query']('', '
+		SELECT *
+		FROM {db_prefix}user_drafts
+		WHERE id_member = {int:id_member}' . ((!empty($topic) && empty($draft_type)) ? '
+			AND id_topic = {int:id_topic}' : (!empty($topic) ? '
+			AND id_reply = {int:id_topic}' : '')) . '
+			AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : '') . '
+		ORDER BY poster_time DESC',
+		array(
+			'id_member' => $member_id,
+			'id_topic' => (int) $topic,
+			'draft_type' => $draft_type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+	
+	// add them to the draft array for display
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// Post drafts
+		if ($draft_type === 0)
+			$context['drafts'][] = array(
+				'subject' => censorText(shorten_subject(stripslashes($row['subject']), 24)),
+				'poster_time' => timeformat($row['poster_time']),
+				'link' => '<a href="' . $scripturl . '?action=post;board=' . $row['id_board'] . ';' . (!empty($row['id_topic']) ? 'topic='. $row['id_topic'] .'.0;' : '') . 'id_draft=' . $row['id_draft'] . '">' . $row['subject'] . '</a>',
+			);
+		// PM drafts
+		elseif ($draft_type === 1)
+			$context['drafts'][] = array(
+				'subject' => censorText(shorten_subject(stripslashes($row['subject']), 24)),
+				'poster_time' => timeformat($row['poster_time']),
+				'link' => '<a href="' . $scripturl . '?action=pm;sa=send;id_draft=' . $row['id_draft'] . '">' . (!empty($row['subject']) ? $row['subject'] : $txt['drafts_none']) . '</a>',
+			);
+	}
+	$smcFunc['db_free_result']($request);
+}
+
+/**
+ * Returns an xml response to an autosave ajax request
+ * provides the id of the draft saved and the time it was saved
+ *
+ * @param type $id_draft
+ */
+function XmlDraft($id_draft)
+{
+	global $txt, $context;
+
+	header('Content-Type: text/xml; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
+
+	echo '<?xml version="1.0" encoding="', $context['character_set'], '"?>
+	<drafts>
+		<draft id="', $id_draft, '"><![CDATA[', $txt['draft_saved_on'], ': ', timeformat(time()), ']]></draft>
+	</drafts>';
+
+	obExit(false);
+}
+
+/**
+ * Show all drafts of a given type by the current user
+ * Uses the showdraft template
+ * Allows for the deleting and loading/editing of drafts
+ *
+ * @param type $memID
+ * @param type $draft_type
+ */
+function showProfileDrafts($memID, $draft_type = 0)
+{
+	global $txt, $user_info, $scripturl, $modSettings, $context, $smcFunc;
+
+	// Some initial context.
+	$context['start'] = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
+	$context['current_member'] = $memID;
+	
+	// If just deleting a draft, do it and then redirect back.
+	if (!empty($_REQUEST['delete']))
+	{
+		checkSession('get');
+		$id_delete = (int) $_REQUEST['delete'];
+		
+		$smcFunc['db_query']('', '
+			DELETE FROM {db_prefix}user_drafts
+			WHERE id_draft = {int:id_draft}
+				AND id_member = {int:id_member}
+				AND type = {int:draft_type}
+			LIMIT 1',
+			array(
+				'id_draft' => $id_delete,
+				'id_member' => $memID,
+				'draft_type' => $draft_type,
+			)
+		);
+
+		redirectexit('action=profile;u=' . $memID . ';area=showdrafts;start=' . $context['start']);
+	}
+
+	// Default to 10.
+	if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount']))
+		$_REQUEST['viewscount'] = '10';
+
+	// Get the count of applicable drafts on the boards they can (still) see ...
+	// @todo .. should we just let them see their drafts even if they have lost board access ?
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(id_draft)
+		FROM {db_prefix}user_drafts AS ud
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board})
+		WHERE id_member = {int:id_member}
+			AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : ''),
+		array(
+			'id_member' => $memID,
+			'draft_type' => $draft_type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+	list ($msgCount) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+	
+	$maxIndex = (int) $modSettings['defaultMaxMessages'];
+
+	// Make sure the starting place makes sense and construct our friend the page index.
+	$context['page_index'] = constructPageIndex($scripturl . '?action=profile;u=' . $memID . ';area=showdrafts', $context['start'], $msgCount, $maxIndex);
+	$context['current_page'] = $context['start'] / $maxIndex;
+
+	// Reverse the query if we're past 50% of the pages for better performance.
+	$start = $context['start'];
+	$reverse = $_REQUEST['start'] > $msgCount / 2;
+	if ($reverse)
+	{
+		$maxIndex = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : (int) $modSettings['defaultMaxMessages'];
+		$start = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 || $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] ? 0 : $msgCount - $context['start'] - $modSettings['defaultMaxMessages'];
+	}
+
+	// Find this user's drafts for the boards they can access
+	// @todo ... do we want to do this?  If they were able to create a draft, do we remove thier access to said draft if they loose 
+	//           access to the board or if the topic moves to a board they can not see?
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			b.id_board, b.name AS bname,
+			ud.id_member, ud.id_draft, ud.body, ud.smileys_enabled, ud.subject, ud.poster_time, ud.icon, ud.id_topic, ud.locked, ud.is_sticky
+		FROM {db_prefix}user_drafts AS ud
+			INNER JOIN {db_prefix}boards AS b ON (b.id_board = ud.id_board AND {query_see_board})
+		WHERE ud.id_member = {int:current_member}
+			AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : '') . '
+		ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . '
+		LIMIT ' . $start . ', ' . $maxIndex,
+		array(
+			'current_member' => $memID,
+			'draft_type' => $draft_type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+
+	// Start counting at the number of the first message displayed.
+	$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
+	$context['posts'] = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// Censor....
+		if (empty($row['body']))
+			$row['body'] = '';
+
+		$row['subject'] = $smcFunc['htmltrim']($row['subject']);
+		if (empty($row['subject']))
+			$row['subject'] = $txt['no_subject'];
+
+		censorText($row['body']);
+		censorText($row['subject']);
+
+		// BBC-ilize the message.
+		$row['body'] = parse_bbc($row['body'], $row['smileys_enabled'], 'draft' . $row['id_draft']);
+
+		// And the array...
+		$context['drafts'][$counter += $reverse ? -1 : 1] = array(
+			'body' => $row['body'],
+			'counter' => $counter,
+			'alternate' => $counter % 2,
+			'board' => array(
+				'name' => $row['bname'],
+				'id' => $row['id_board']
+			),
+			'topic' => array(
+				'id' => $row['id_topic'],
+				'link' => empty($row['id']) ? $row['subject'] : '<a href="' . $scripturl . '?topic=' . $row['id_topic'] . '.0">' . $row['subject'] . '</a>',
+			),
+			'subject' => $row['subject'],
+			'time' => timeformat($row['poster_time']),
+			'timestamp' => forum_time(true, $row['poster_time']),
+			'icon' => $row['icon'],
+			'id_draft' => $row['id_draft'],
+			'locked' => $row['locked'],
+			'sticky' => $row['is_sticky'],
+		);
+	}
+	$smcFunc['db_free_result']($request);
+
+	// All drafts were retrieved in reverse order, get them right again.
+	if ($reverse)
+		$context['drafts'] = array_reverse($context['drafts'], true);
+
+	$context['sub_template'] = 'showDrafts';
+}
+
+/**
+ * Show all PM drafts of the current user
+ * Uses the showpmdraft template
+ * Allows for the deleting and loading/editing of drafts
+ *
+ * @param type $memID
+ * @param type $draft_type
+ */
+function showPMDrafts($memID = -1)
+{
+	global $txt, $user_info, $scripturl, $modSettings, $context, $smcFunc;
+	
+	// init
+	$draft_type = 1;
+
+	// If just deleting a draft, do it and then redirect back.
+	if (!empty($_REQUEST['delete']))
+	{
+		checkSession('get');
+		$id_delete = (int) $_REQUEST['delete'];
+		$start = isset($_REQUEST['start']) ? (int) $_REQUEST['start'] : 0;
+
+		$smcFunc['db_query']('', '
+			DELETE FROM {db_prefix}user_drafts
+			WHERE id_draft = {int:id_draft}
+				AND id_member = {int:id_member}
+				AND type = {int:draft_type}
+			LIMIT 1',
+			array(
+				'id_draft' => $id_delete,
+				'id_member' => $memID,
+				'draft_type' => $draft_type,
+			)
+		);
+
+		// now redirect back to the list
+		redirectexit('action=pm;sa=showpmdrafts;start=' . $start);
+	}
+	
+	// perhaps a draft was selected for editing? if so pass this off
+	if (!empty($_REQUEST['id_draft']) && !empty($context['drafts_pm_save']) && $memID == $user_info['id'])
+	{
+		checkSession('get');
+		$draft_id = (int) $_REQUEST['id_draft'];
+		redirectexit('action=pm;sa=send;id_draft=' . $draft_id);
+	}
+
+	// Default to 10.
+	if (empty($_REQUEST['viewscount']) || !is_numeric($_REQUEST['viewscount']))
+		$_REQUEST['viewscount'] = '10';
+
+	// Get the count of applicable drafts
+	$request = $smcFunc['db_query']('', '
+		SELECT COUNT(id_draft)
+		FROM {db_prefix}user_drafts AS ud
+		WHERE id_member = {int:id_member}
+			AND type={int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : ''),
+		array(
+			'id_member' => $memID,
+			'draft_type' => $draft_type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+	list ($msgCount) = $smcFunc['db_fetch_row']($request);
+	$smcFunc['db_free_result']($request);
+
+	$maxIndex = (int) $modSettings['defaultMaxMessages'];
+
+	// Make sure the starting place makes sense and construct our friend the page index.
+	$context['page_index'] = constructPageIndex($scripturl . '?action=pm;sa=showpmdrafts', $context['start'], $msgCount, $maxIndex);
+	$context['current_page'] = $context['start'] / $maxIndex;
+
+	// Reverse the query if we're past 50% of the total for better performance.
+	$start = $context['start'];
+	$reverse = $_REQUEST['start'] > $msgCount / 2;
+	if ($reverse)
+	{
+		$maxIndex = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 && $msgCount > $context['start'] ? $msgCount - $context['start'] : (int) $modSettings['defaultMaxMessages'];
+		$start = $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] + 1 || $msgCount < $context['start'] + $modSettings['defaultMaxMessages'] ? 0 : $msgCount - $context['start'] - $modSettings['defaultMaxMessages'];
+	}
+
+	// Load in this user's PM drafts
+	$request = $smcFunc['db_query']('', '
+		SELECT
+			ud.id_member, ud.id_draft, ud.body, ud.subject, ud.poster_time, ud.outbox, ud.id_reply, ud.to_list
+		FROM {db_prefix}user_drafts AS ud
+		WHERE ud.id_member = {int:current_member}
+			AND type = {int:draft_type}' . (!empty($modSettings['drafts_keep_days']) ? '
+			AND poster_time > {int:time}' : '') . '
+		ORDER BY ud.id_draft ' . ($reverse ? 'ASC' : 'DESC') . '
+		LIMIT ' . $start . ', ' . $maxIndex,
+		array(
+			'current_member' => $memID,
+			'draft_type' => $draft_type,
+			'time' => (!empty($modSettings['drafts_keep_days']) ? (time() - ($modSettings['drafts_keep_days'] * 86400)) : 0),
+		)
+	);
+	
+	// Start counting at the number of the first message displayed.
+	$counter = $reverse ? $context['start'] + $maxIndex + 1 : $context['start'];
+	$context['posts'] = array();
+	while ($row = $smcFunc['db_fetch_assoc']($request))
+	{
+		// Censor....
+		if (empty($row['body']))
+			$row['body'] = '';
+
+		$row['subject'] = $smcFunc['htmltrim']($row['subject']);
+		if (empty($row['subject']))
+			$row['subject'] = $txt['no_subject'];
+
+		censorText($row['body']);
+		censorText($row['subject']);
+
+		// BBC-ilize the message.
+		$row['body'] = parse_bbc($row['body'], true, 'draft' . $row['id_draft']);
+		
+		// Have they provide who this will go to?
+		$recipients = array(
+			'to' => array(),
+			'bcc' => array(),
+		);
+		$recipient_ids = (!empty($row['to_list'])) ? unserialize($row['to_list']) : array();
+		
+		// @todo ... this is a bit ugly since it runs an extra query for every message, do we want this?
+		// at least its only for draft PM's and only the user can see them ... so not heavily used .. still
+		if (!empty($recipient_ids['to']) || !empty($recipient_ids['bcc']))
+		{
+			$recipient_ids['to'] = array_map('intval', $recipient_ids['to']);
+			$recipient_ids['bcc'] = array_map('intval', $recipient_ids['bcc']);
+			$allRecipients = array_merge($recipient_ids['to'], $recipient_ids['bcc']);
+
+			$request_2 = $smcFunc['db_query']('', '
+				SELECT id_member, real_name
+				FROM {db_prefix}members
+				WHERE id_member IN ({array_int:member_list})',
+				array(
+					'member_list' => $allRecipients,
+				)
+			);
+			while ($result = $smcFunc['db_fetch_assoc']($request_2))
+			{
+				$recipientType = in_array($result['id_member'], $recipient_ids['bcc']) ? 'bcc' : 'to';
+				$recipients[$recipientType][] = $result['real_name'];
+			}
+			$smcFunc['db_free_result']($request_2);
+		}
+
+		// Add the items to the array for template use
+		$context['drafts'][$counter += $reverse ? -1 : 1] = array(
+			'body' => $row['body'],
+			'counter' => $counter,
+			'alternate' => $counter % 2,
+			'subject' => $row['subject'],
+			'time' => timeformat($row['poster_time']),
+			'timestamp' => forum_time(true, $row['poster_time']),
+			'id_draft' => $row['id_draft'],
+			'recipients' => $recipients,
+			'age' => floor((time() - $row['poster_time']) / 86400),
+			'remaining' => (!empty($modSettings['drafts_keep_days']) ? floor($modSettings['drafts_keep_days'] - ((time() - $row['poster_time']) / 86400)) : 0),
+		);
+	}
+	$smcFunc['db_free_result']($request);
+	
+	// if the drafts were retrieved in reverse order, then put them in the right order again.
+	if ($reverse)
+		$context['drafts'] = array_reverse($context['drafts'], true);
+		
+	// off to the template we go
+	$context['page_title'] = $txt['drafts'];
+	$context['sub_template'] = 'showPMDrafts';
+	$context['linktree'][] = array(
+		'url' => $scripturl . '?action=pm;sa=showpmdrafts',
+		'name' => $txt['drafts'],
+	);
+}
+
+/**
+ * Modify any setting related to drafts.
+ * Requires the admin_forum permission.
+ * Accessed from ?action=admin;area=managedrafts
+ *
+ * @param bool $return_config = false
+ * @uses Admin template, edit_topic_settings sub-template.
+ */
+function ModifyDraftSettings($return_config = false)
+{
+	global $context, $txt, $sourcedir, $scripturl;
+
+	isAllowedTo('admin_forum');
+
+	// Here are all the draft settings, a bit lite for now, but we can add more :P
+	$config_vars = array(
+			// Draft settings ...
+			array('check', 'drafts_post_enabled'),
+			array('check', 'drafts_pm_enabled'),
+			array('check', 'drafts_show_saved_enabled', 'subtext' => $txt['drafts_show_saved_enabled_subnote']),
+			array('int', 'drafts_keep_days', 'postinput' => $txt['days_word'], 'subtext' => $txt['drafts_keep_days_subnote']),
+			'',
+			array('check', 'drafts_autosave_enabled', 'subtext' => $txt['drafts_autosave_enabled_subnote']),
+			array('int', 'drafts_autosave_frequency', 'postinput' => $txt['manageposts_seconds'], 'subtext' => $txt['drafts_autosave_frequency_subnote']),
+	);
+
+	if ($return_config)
+		return $config_vars;
+
+	// Get the settings template ready.
+	require_once($sourcedir . '/ManageServer.php');
+
+	// Setup the template.
+	$context['page_title'] = $txt['managedrafts_settings'];
+	$context['sub_template'] = 'show_settings';
+
+	// Saving them ?
+	if (isset($_GET['save']))
+	{
+		checkSession();
+
+		// Protect them from themselves.
+		$_POST['drafts_autosave_frequency'] = $_POST['drafts_autosave_frequency'] < 30 ? 30 : $_POST['drafts_autosave_frequency'];
+		saveDBSettings($config_vars);
+		redirectexit('action=admin;area=managedrafts');
+	}
+
+	// some javascript to enable / disable the frequency input box
+	$context['settings_post_javascript'] = '
+		var autosave = document.getElementById(\'drafts_autosave_enabled\');
+		mod_addEvent(autosave, \'change\', toggle);
+		toggle();
+
+		function mod_addEvent(control, ev, fn)
+		{
+			if (control.addEventListener)
+			{
+				control.addEventListener(ev, fn, false);
+			}
+			else if (control.attachEvent)
+			{
+				control.attachEvent(\'on\'+ev, fn);
+			}
+		}
+		function toggle()
+		{
+			var select_elem = document.getElementById(\'drafts_autosave_frequency\');
+			select_elem.disabled = !autosave.checked;
+		}
+	';
+
+	// Final settings...
+	$context['post_url'] = $scripturl . '?action=admin;area=managedrafts;save';
+	$context['settings_title'] = $txt['managedrafts_settings'];
+
+	// Prepare the settings...
+	prepareDBSettingContext($config_vars);
+}
+
+?>

+ 50 - 38
Sources/Load.php

@@ -940,7 +940,7 @@ function loadMemberData($users, $is_name = false, $set = 'normal')
 
 	// Allow mods to easily add to the selected member data
 	call_integration_hook('integrate_load_member_data', array(&$select_columns, &$select_tables));
-	
+
 	if (!empty($users))
 	{
 		// Load the member's data.
@@ -1645,7 +1645,7 @@ function loadTheme($id_theme = 0, $initialize = true)
 		// If not a user variant, select the default.
 		if ($context['theme_variant'] == '' || !in_array($context['theme_variant'], $settings['theme_variants']))
 			$context['theme_variant'] = !empty($settings['default_variant']) && in_array($settings['default_variant'], $settings['theme_variants']) ? $settings['default_variant'] : $settings['theme_variants'][0];
-	
+
 		// Do this to keep things easier in the templates.
 		$context['theme_variant'] = '_' . $context['theme_variant'];
 		$context['theme_variant_url'] = $context['theme_variant'] . '/';
@@ -1703,23 +1703,23 @@ function loadTheme($id_theme = 0, $initialize = true)
 		'ajax_notification_cancel_text' => JavaScriptEscape($txt['modify_cancel']),
 		'help_popup_heading_text' => JavaScriptEscape($txt['help_popup']),
 	);
-	
+
 	// Add the JQuery library to the list of files to load.
 	if (isset($modSettings['jquery_source']) && $modSettings['jquery_source'] == 'cdn')
 		loadJavascriptFile('https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', array(), 'jquery');
 	elseif (isset($modSettings['jquery_source']) && $modSettings['jquery_source'] == 'local')
 		loadJavascriptFile('jquery-1.7.1.min.js', array('default_theme' => true), 'jquery');
-	// Auto load, eh? template_javascript() will take care of the inline half of this.
+	// Auto loading? template_javascript() will take care of the local half of this.
 	else
 		loadJavascriptFile('https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', array(), 'jquery');
-	
+
 	// Queue our JQuery plugins!
-	loadJavascriptFile('smf_jquery_plugins.js', array('default_theme' => true), 'jquery_plugins');
-	
+	loadJavascriptFile('smf_jquery_plugins.js?alp21', array('default_theme' => true), 'jquery_plugins');
+
 	// script.js and theme.js, always required, so always add them! Makes index.template.php cleaner and all.
-	loadJavascriptFile('script.js', array('default_theme' => true), 'smf_scripts');
-	loadJavascriptFile('theme.js', array(), 'theme_scripts');
-	
+	loadJavascriptFile('script.js?alp21', array('default_theme' => true), 'smf_scripts');
+	loadJavascriptFile('theme.js?alp21', array(), 'theme_scripts');
+
 	// If we think we have mail to send, let's offer up some possibilities... robots get pain (Now with scheduled task support!)
 	if ((!empty($modSettings['mail_next_send']) && $modSettings['mail_next_send'] < time() && empty($modSettings['mail_queue_use_cron'])) || empty($modSettings['next_task_time']) || $modSettings['next_task_time'] < time())
 	{
@@ -1785,7 +1785,7 @@ function loadTemplate($template_name, $style_sheets = array(), $fatal = true)
 {
 	global $context, $settings, $txt, $scripturl, $boarddir, $db_show_debug;
 
-	// Do any style sheets first (reroute to the new function to do this!)
+	// Do any style sheets first, cause we're easy with those.
 	if (!empty($style_sheets))
 	{
 		if (!is_array($style_sheets))
@@ -1889,28 +1889,32 @@ function loadSubTemplate($sub_template_name, $fatal = false)
 /**
  * Add a CSS file for output later
  *
- * @param string $filename
- * @param array $options
+ * Options are the following:
  * 	- local (true/false): define if the file is local
  * 	- default_theme (true/false): force use of default theme url
  * 	- force_current (true/false): if this is false, we will attempt to load the file from the default theme if not found in the current theme
+ *  - validate (true/false): if true script will validate the local file exists
+ *
+ * @param string $filename
+ * @param array $options
  * @param string $id
  */
 function loadCSSFile($filename, $options = array(), $id = '')
 {
 	global $settings, $context;
 
-	$theme = !empty($options['default_theme']) ? 'default_theme' : 'theme';
 	$options['force_current'] = !empty($options['force_current']) ? $options['force_current'] : false;
+	$theme = !empty($options['default_theme']) ? 'default_theme' : 'theme';
+	$id = empty($id) ? basename($filename, '.css?alp21') : $id;
 
 	// Is this a local file?
 	if (strpos($filename, 'http') === false || !empty($options['local']))
 	{
-		// Make sure it exists, too!
-		if(!file_exists($settings[$theme . '_dir'] . '/css/' . $filename))
+		// Are we validating the the file exists?
+		if (!empty($options['validate']) && !file_exists($settings[$theme . '_dir'] . '/css/' . $filename))
 		{
 			// Maybe the default theme has it?
-			if($theme == 'theme' && file_exists($settings['default_theme_dir'] . '/' . $filename) && !$options['force_current'])
+			if ($theme === 'theme' && !$options['force_current'] && file_exists($settings['default_theme_dir'] . '/' . $filename))
 				$filename = $settings['default_theme_url'] . '/css/' . $filename;
 			else
 				$filename = false;
@@ -1919,37 +1923,43 @@ function loadCSSFile($filename, $options = array(), $id = '')
 			$filename = $settings[$theme . '_url'] . '/css/' . $filename;
 	}
 
-	if(!empty($filename))
-		$context['css_files'][(empty($id) ? basename($filename) : $id)] = array('filename' => $filename, 'options' => $options);
+	// Add it to the array for use in the template
+	if (!empty($filename))
+		$context['css_files'][$id] = array('filename' => $filename, 'options' => $options);
 }
 
 /**
  * Add a Javascript file for output later
  *
- * @param string $filename
- * @param array $options, possible parameters:
+ * Options are the following:
  * 	- local (true/false): define if the file is local
  * 	- default_theme (true/false): force use of default theme url
- * 	- force_current (true/false): if this is false, we will attempt to load the file from the default theme if not found in the current theme
  * 	- defer (true/false): define if the file should load in <head> or before the closing <html> tag
+ * 	- force_current (true/false): if this is false, we will attempt to load the file from the
+ *    default theme if not found in the current theme
  *	- async (true/false): if the script should be loaded asynchronously (HTML5)
+ *  - validate (true/false): if true script will validate the local file exists
+ *
+ * @param string $filename
+ * @param array $options
  * @param string $id
  */
 function loadJavascriptFile($filename, $options = array(), $id = '')
 {
 	global $settings, $context;
 
-	$theme = !empty($options['default_theme']) ? 'default_theme' : 'theme';
 	$options['force_current'] = !empty($options['force_current']) ? $options['force_current'] : false;
+	$theme = !empty($options['default_theme']) ? 'default_theme' : 'theme';
+	$id = empty($id) ? basename($filename, '.js?alp21') : $id;
 
 	// Is this a local file?
 	if (strpos($filename, 'http') === false || !empty($options['local']))
 	{
-		// Make sure it exists, too!
-		if(!file_exists($settings[$theme . '_dir'] . '/scripts/' . $filename))
+		// Are we validating it exists on disk?
+		if (!empty($options['validate']) && !file_exists($settings[$theme . '_dir'] . '/scripts/' . $filename))
 		{
-			// Maybe the default theme has it?
-			if($theme == 'theme' && file_exists($settings['default_theme_dir'] . '/' . $filename) && !$options['force_current'])
+			// can't find it in this theme, how about the default?
+			if ($theme === 'theme' && !$options['force_current'] && file_exists($settings['default_theme_dir'] . '/' . $filename))
 				$filename = $settings['default_theme_url'] . '/scripts/' . $filename;
 			else
 				$filename = false;
@@ -1958,8 +1968,9 @@ function loadJavascriptFile($filename, $options = array(), $id = '')
 			$filename = $settings[$theme . '_url'] . '/scripts/' . $filename;
 	}
 
-	if(!empty($filename))
-		$context['javascript_files'][(empty($id) ? basename($filename) : $id)] = array('filename' => $filename, 'options' => $options);
+	// Add it to the array for use in the template
+	if (!empty($filename))
+		$context['javascript_files'][$id] = array('filename' => $filename, 'options' => $options);
 }
 
 /**
@@ -1974,15 +1985,16 @@ function addJavascriptVar($key, $value, $escape = false)
 {
 	global $context;
 
-	if(!empty($key) && !empty($value))
-		$context['javascript_vars'][$key] = $escape ? JavaScriptEscape($value) : $value;
+	if (!empty($key) && !empty($value))
+		$context['javascript_vars'][$key] = !empty($escape) ? JavaScriptEscape($value) : $value;
 }
 
 /**
  * Add a block of inline Javascript code to be executed later
- * Only use this if you have to, generally external JS files are better, but for very small scripts
- * or for scripts that require help from PHP/whatever, this can be useful.
- * Do note that all code added with this function is added to the same <script> tag so do make sure your JS is clean!
+ *
+ * - only use this if you have to, generally external JS files are better, but for very small scripts
+ *   or for scripts that require help from PHP/whatever, this can be useful.
+ * - all code added with this function is added to the same <script> tag so do make sure your JS is clean!
  *
  * @param string $javascript
  * @param bool $defer = false, define if the script should load in <head> or before the closing <html> tag
@@ -1991,8 +2003,8 @@ function addInlineJavascript($javascript, $defer = false)
 {
 	global $context;
 
-	if(!empty($javascript))
-		$context['javascript_inline'][$defer ? 'defer' : 'standard'][] = $javascript;
+	if (!empty($javascript))
+		$context['javascript_inline'][(!empty($defer) ? 'defer' : 'standard')][] = $javascript;
 }
 
 /**
@@ -2589,8 +2601,8 @@ function cache_quick_get($key, $file, $function, $params, $level = 1)
 /**
  * Puts value in the cache under key for ttl seconds.
  *
- * - It may "miss" so shouldn't be depended on 
- * - Uses the cahce engine chosen in the ACP and saved in settings.php
+ * - It may "miss" so shouldn't be depended on
+ * - Uses the cache engine chosen in the ACP and saved in settings.php
  * - It supports:
  *     Turck MMCache: http://turck-mmcache.sourceforge.net/index_old.html#api
  *     Xcache: http://xcache.lighttpd.net/wiki/XcacheApi

+ 45 - 11
Sources/ManageMaintenance.php

@@ -82,6 +82,7 @@ function ManageMaintenance()
 			'activities' => array(
 				'massmove' => 'MaintainMassMoveTopics',
 				'pruneold' => 'MaintainRemoveOldPosts',
+				'olddrafts' => 'MaintainRemoveOldDrafts',
 			),
 		),
 		'destroy' => array(
@@ -163,7 +164,7 @@ function MaintainDatabase()
 	$memory_limit = memoryReturnBytes(ini_get('memory_limit')) / (1024 * 1024);
 	// Zip limit is set to more or less 1/4th the size of the available memory * 1500
 	// 1500 is an estimate of the number of messages that generates a database of 1 MB (yeah I know IT'S AN ESTIMATION!!!)
-	// Why that? Because the only reliable zip package is the one sent out the first time, 
+	// Why that? Because the only reliable zip package is the one sent out the first time,
 	// so when the backup takes 1/5th (just to stay on the safe side) of the memory available
 	$zip_limit = $memory_limit * 1500 / 5;
 	// Here is more tricky: it depends on many factors, but the main idea is that
@@ -243,7 +244,7 @@ function MaintainMembers()
 		);
 	}
 	$smcFunc['db_free_result']($result);
-	
+
 	if (isset($_GET['done']) && $_GET['done'] == 'recountposts')
 		$context['maintenance_finished'] = $txt['maintain_recountposts'];
 }
@@ -1191,7 +1192,7 @@ function AdminBoardRecount()
 
 	isAllowedTo('admin_forum');
 	checkSession('request');
-	
+
 	// validate the request or the loop
 	if (!isset($_REQUEST['step']))
 		validateToken('admin-maint');
@@ -1881,6 +1882,39 @@ function MaintainRemoveOldPosts()
 	RemoveOldTopics2();
 }
 
+/**
+ * Removing old drafts
+ */
+function MaintainRemoveOldDrafts()
+{
+	global $sourcedir, $smcFunc;
+
+	validateToken('admin-maint');
+
+	$drafts = array();
+
+	// Find all of the old drafts
+	$request = $smcFunc['db_query']('', '
+		SELECT id_draft
+		FROM {db_prefix}user_drafts
+		WHERE poster_time <= {int:poster_time_old}',
+		array(
+			'poster_time_old' => time() - (86400 * $_POST['draftdays']),
+		)
+	);
+
+	while ($row = $smcFunc['db_fetch_row']($request))
+		$drafts[] = $row[0];
+	$smcFunc['db_free_result']($request);
+
+	// If we have old one, remove them
+	if (count($drafts) > 0)
+	{
+		require_once($sourcedir . '/Drafts.php');
+		DeleteDraft($drafts, false);
+	}
+}
+
 /**
  * Moves topics from one board to another.
  *
@@ -2017,7 +2051,7 @@ function MaintainRecountPosts()
 	$context['continue_countdown'] = 3;
 	$context['continue_get_data'] = '';
 	$context['sub_template'] = 'not_done';
-	
+
 	// init
 	$increment = 200;
 	$_REQUEST['start'] = !isset($_REQUEST['start']) ? 0 : (int) $_REQUEST['start'];
@@ -2029,7 +2063,7 @@ function MaintainRecountPosts()
 	if (!isset($_SESSION['total_members']))
 	{
 		validateToken('admin-maint');
-		
+
 		$request = $smcFunc['db_query']('', '
 			SELECT COUNT(DISTINCT m.id_member)
 			FROM ({db_prefix}messages AS m, {db_prefix}boards AS b)
@@ -2087,7 +2121,7 @@ function MaintainRecountPosts()
 		$_REQUEST['start'] += $increment;
 		$context['continue_get_data'] = '?action=admin;area=maintain;sa=members;activity=recountposts;start=' . $_REQUEST['start'] . ';' . $context['session_var'] . '=' . $context['session_id'];
 		$context['continue_percent'] = round(100 * $_REQUEST['start'] / $_SESSION['total_members']);
-		
+
 		createToken('admin-recountposts');
 		$context['continue_post_data'] = '<input type="hidden" name="' . $context['admin-recountposts_token_var'] . '" value="' . $context['admin-recountposts_token'] . '" />';
 
@@ -2095,7 +2129,7 @@ function MaintainRecountPosts()
 			apache_reset_timeout();
 		return;
 	}
-	
+
 	// final steps ... made more difficult since we don't yet support sub-selects on joins
 	// place all members who have posts in the message table in a temp table
 	$createTemporary = $smcFunc['db_query']('', '
@@ -2103,7 +2137,7 @@ function MaintainRecountPosts()
 			id_member mediumint(8) unsigned NOT NULL default {string:string_zero},
 			PRIMARY KEY (id_member)
 		)
-		SELECT m.id_member 
+		SELECT m.id_member
 		FROM ({db_prefix}messages AS m,{db_prefix}boards AS b)
 		WHERE m.id_member != {int:zero}
 			AND b.count_posts = {int:zero}
@@ -2117,7 +2151,7 @@ function MaintainRecountPosts()
 		)
 	) !== false;
 
-	if ($createTemporary) 
+	if ($createTemporary)
 	{
 		// outer join the members table on the temporary table finding the members that have a post count but no posts in the message table
 		$request = $smcFunc['db_query']('', '
@@ -2125,7 +2159,7 @@ function MaintainRecountPosts()
 			FROM {db_prefix}members AS mem
 			LEFT OUTER JOIN {db_prefix}tmp_maint_recountposts AS res
 			ON res.id_member = mem.id_member
-			WHERE res.id_member IS null 
+			WHERE res.id_member IS null
 				AND mem.posts != {int:zero}',
 			array(
 				'zero' => 0,
@@ -2147,7 +2181,7 @@ function MaintainRecountPosts()
 		}
 		$smcFunc['db_free_result']($request);
 	}
-	
+
 	// all done
 	unset($_SESSION['total_members']);
 	$context['maintenance_finished'] = $txt['maintain_recountposts'];

+ 6 - 0
Sources/ManagePermissions.php

@@ -1463,6 +1463,8 @@ function loadAllPermissions($loadType = 'classic')
 			'disable_censor' => array(false, 'general', 'disable_censor'),
 			'pm_read' => array(false, 'pm', 'use_pm_system'),
 			'pm_send' => array(false, 'pm', 'use_pm_system'),
+			'pm_draft' => array(false, 'pm', 'use_pm_system'),
+			'pm_autosave_draft' => array(false, 'pm', 'use_pm_system'),
 			'send_email_to_members' => array(false, 'pm', 'use_pm_system'),
 			'calendar_view' => array(false, 'calendar', 'view_basic_info'),
 			'calendar_post' => array(false, 'calendar', 'post_calendar'),
@@ -1492,6 +1494,8 @@ function loadAllPermissions($loadType = 'classic')
 			'moderate_board' => array(false, 'general_board', 'moderate'),
 			'approve_posts' => array(false, 'general_board', 'moderate'),
 			'post_new' => array(false, 'topic', 'make_posts'),
+			'post_draft' => array(false, 'topic', 'make_posts'),
+			'post_autosave_draft' => array(false, 'topic', 'make_posts'),
 			'post_unapproved_topics' => array(false, 'topic', 'make_unapproved_posts'),
 			'post_unapproved_replies' => array(true, 'topic', 'make_unapproved_posts', 'make_unapproved_posts'),
 			'post_reply' => array(true, 'topic', 'make_posts', 'make_posts'),
@@ -2249,6 +2253,8 @@ function loadIllegalGuestPermissions()
 		'modify_replies',
 		'send_mail',
 		'approve_posts',
+		'post_draft',
+		'post_autosave_draft',
 	);
 
 	call_integration_hook('integrate_load_illegal_guest_permissions');

+ 25 - 0
Sources/ManageSettings.php

@@ -225,6 +225,31 @@ function ModifyCoreFeatures($return_config = false)
 					return array();
 			'),
 		),
+		// dr = drafts
+		'dr' => array(
+			'url' => 'action=admin;area=managedrafts',
+			'settings' => array(
+				'drafts_enabled' => 1,
+				'drafts_post_enabled' => 2,
+				'drafts_pm_enabled' => 2,
+				'drafts_autosave_enabled' => 2,
+				'drafts_show_saved_enabled' => 2,
+			),
+			'setting_callback' => create_function('$value', '
+				global $smcFunc, $sourcedir;
+
+				// Set the correct disabled value for the scheduled task.
+				$smcFunc[\'db_query\'](\'\', \'
+					UPDATE {db_prefix}scheduled_tasks
+					SET disabled = {int:disabled}
+					WHERE task = {string:task}\',
+					array(
+						\'disabled\' => $value ? 0 : 1,
+						\'task\' => \'remove_old_drafts\',
+					)
+				);
+			'),
+		),
 		// k = karma.
 		'k' => array(
 			'url' => 'action=admin;area=featuresettings;sa=karma',

+ 68 - 12
Sources/PersonalMessage.php

@@ -35,7 +35,7 @@ function MessageMain()
 	// This file contains the basic functions for sending a PM.
 	require_once($sourcedir . '/Subs-Post.php');
 
-	loadLanguage('PersonalMessage');
+	loadLanguage('PersonalMessage+Drafts');
 
 	if (WIRELESS && WIRELESS_PROTOCOL == 'wap')
 		fatal_lang_error('wireless_error_notyet', false);
@@ -197,6 +197,7 @@ function MessageMain()
 		'send' => 'MessagePost',
 		'send2' => 'MessagePost2',
 		'settings' => 'MessageSettings',
+		'showpmdrafts' => 'MessageDrafts',
 	);
 
 	if (!isset($_REQUEST['sa']) || !isset($subActions[$_REQUEST['sa']]))
@@ -210,7 +211,7 @@ function MessageMain()
 }
 
 /**
- * A sidebar to easily access different areas of the section
+ * A menu to easily access different areas of the PM section
  *
  * @param string $area
  */
@@ -235,6 +236,12 @@ function messageIndexBar($area)
 					'label' => $txt['sent_items'],
 					'custom_url' => $scripturl . '?action=pm;f=sent',
 				),
+				'drafts' => array(
+					'label' => $txt['drafts_show'],
+					'custom_url' => $scripturl . '?action=pm;sa=showpmdrafts',
+					'permission' => allowedTo('pm_draft'),
+					'enabled' => !empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_pm_enabled']),
+				),
 			),
 		),
 		'labels' => array(
@@ -272,7 +279,7 @@ function messageIndexBar($area)
 			),
 		),
 	);
-
+	
 	// Handle labels.
 	if (empty($context['currently_using_labels']))
 		unset($pm_areas['labels']);
@@ -300,7 +307,7 @@ function messageIndexBar($area)
 		if (!empty($unread_in_labels))
 			$pm_areas['labels']['title'] .= ' (' . $unread_in_labels . ')';
 	}
-
+	
 	$pm_areas['folders']['areas']['inbox']['unread_messages'] = &$context['labels'][-1]['unread_messages'];
 	$pm_areas['folders']['areas']['inbox']['messages'] = &$context['labels'][-1]['messages'];
 	if (!empty($context['labels'][-1]['unread_messages']))
@@ -335,19 +342,24 @@ function messageIndexBar($area)
 		'toggle_url' => $current_page . ';togglebar',
 		'toggle_redirect_url' => $current_page,
 	);
-
+	
 	// Actually create the menu!
 	$pm_include_data = createMenu($pm_areas, $menuOptions);
 	unset($pm_areas);
+	
+	// No menu means no access.
+	if (!$pm_include_data && (!$user_info['is_guest'] || validateSession()))
+		fatal_lang_error('no_access', false);
 
 	// Make a note of the Unique ID for this menu.
 	$context['pm_menu_id'] = $context['max_menu_id'];
 	$context['pm_menu_name'] = 'menu_data_' . $context['pm_menu_id'];
 
 	// Set the selected item.
-	$context['menu_item_selected'] = $pm_include_data['current_area'];
+	$current_area = $pm_include_data['current_area'];
+	$context['menu_item_selected'] = $current_area;
 
-	// obExit will know what to do!
+	// Set the template for this area and add the profile layer.
 	if (!WIRELESS && !isset($_REQUEST['xml']))
 		$context['template_layers'][] = 'pm';
 }
@@ -1770,6 +1782,18 @@ function MessagePost()
 	);
 
 	$modSettings['disable_wysiwyg'] = !empty($modSettings['disable_wysiwyg']) || empty($modSettings['enableBBC']);
+	
+	// Are PM drafts enabled?
+	$context['drafts_pm_save'] = !empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_pm_enabled']) && allowedTo('pm_draft');
+	$context['drafts_autosave'] = !empty($context['drafts_pm_save']) && !empty($modSettings['drafts_autosave_enabled']) && allowedTo('pm_autosave_draft');
+	
+	// Generate a list of drafts that they can load in to the editor
+	if (!empty($context['drafts_pm_save']))
+	{
+		require_once($sourcedir . '/Drafts.php');
+		$pm_seed = isset($_REQUEST['pmsg']) ? $_REQUEST['pmsg'] : (isset($_REQUEST['quote']) ? $_REQUEST['quote'] : 0);
+		ShowDrafts($user_info['id'], $pm_seed, 1);
+	}
 
 	// Needed for the WYSIWYG editor.
 	require_once($sourcedir . '/Subs-Editor.php');
@@ -1806,6 +1830,28 @@ function MessagePost()
 	checkSubmitOnce('register');
 }
 
+/**
+ * This function allows the user to view their PM drafts
+ */
+function MessageDrafts()
+{
+	global $context, $sourcedir, $user_info, $modSettings;
+	
+	// Set draft capability
+	$context['drafts_pm_save'] = !empty($modSettings['drafts_pm_enabled']) && allowedTo('pm_draft');
+	$context['drafts_autosave'] = !empty($context['drafts_pm_save']) && !empty($modSettings['drafts_autosave_enabled']) && allowedTo('pm_autosave_draft');
+	
+	// validate with loadMemberData()
+	$memberResult = loadMemberData($user_info['id'], false);
+	if (!is_array($memberResult))
+		fatal_lang_error('not_a_user', false);
+	list ($memID) = $memberResult;
+	
+	// drafts is where the functions reside
+	require_once($sourcedir . '/Drafts.php');
+	showPMDrafts($memID);
+}
+
 /**
  * An error in the message...
  *
@@ -1946,6 +1992,10 @@ function messagePostError($error_types, $named_recipients, $recipient_ids = arra
 			$context['error_type'] = 'serious';
 	}
 
+	// Need to reset draft capability once again
+	$context['drafts_pm_save'] = !empty($modSettings['drafts_pm_enabled']) && allowedTo('pm_draft');
+	$context['drafts_autosave'] = !empty($context['drafts_pm_save']) && !empty($modSettings['drafts_autosave_enabled']) && allowedTo('pm_autosave_draft');
+
 	// We need to load the editor once more.
 	require_once($sourcedir . '/Subs-Editor.php');
 
@@ -2146,7 +2196,7 @@ function MessagePost2()
 		// Preparse the message.
 		$message = $_REQUEST['message'];
 		preparsecode($message);
-
+		
 		// Make sure there's still some content left without the tags.
 		if ($smcFunc['htmltrim'](strip_tags(parse_bbc($smcFunc['htmlspecialchars']($message, ENT_QUOTES), false), '<img>')) === '' && (!allowedTo('admin_forum') || strpos($message, '[html]') === false))
 			$post_errors[] = 'no_message';
@@ -2162,15 +2212,13 @@ function MessagePost2()
 		$context['require_verification'] = create_control_verification($verificationOptions, true);
 
 		if (is_array($context['require_verification']))
-		{
 			$post_errors = array_merge($post_errors, $context['require_verification']);
-		}
 	}
 
 	// If they did, give a chance to make ammends.
 	if (!empty($post_errors) && !$is_recipient_change && !isset($_REQUEST['preview']) && !isset($_REQUEST['xml']))
 		return messagePostError($post_errors, $namedRecipientList, $recipientList);
-
+		
 	// Want to take a second glance before you send?
 	if (isset($_REQUEST['preview']))
 	{
@@ -2206,6 +2254,14 @@ function MessagePost2()
 
 		return messagePostError(array(), $namedRecipientList, $recipientList);
 	}
+	
+	// Want to save this as a draft and think about it some more?
+	if (!empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_pm_enabled']) && isset($_POST['save_draft']))
+	{
+		require_once($sourcedir . '/Drafts.php');
+		SavePMDraft($post_errors, $recipientList);
+		return messagePostError($post_errors, $namedRecipientList, $recipientList);
+	}
 
 	// Before we send the PM, let's make sure we don't have an abuse of numbers.
 	elseif (!empty($modSettings['max_pm_recipients']) && count($recipientList['to']) + count($recipientList['bcc']) > $modSettings['max_pm_recipients'] && !allowedTo(array('moderate_forum', 'send_mail', 'admin_forum')))
@@ -2222,7 +2278,7 @@ function MessagePost2()
 
 	// Prevent double submission of this form.
 	checkSubmitOnce('check');
-
+	
 	// Do the actual sending of the PM.
 	if (!empty($recipientList['to']) || !empty($recipientList['bcc']))
 		$context['send_log'] = sendpm($recipientList, $_REQUEST['subject'], $_REQUEST['message'], !empty($_REQUEST['outbox']), null, !empty($_REQUEST['pm_head']) ? (int) $_REQUEST['pm_head'] : 0);

+ 38 - 3
Sources/Post.php

@@ -464,7 +464,7 @@ function Post($post_errors = array())
 		}
 
 		// Only show the preview stuff if they hit Preview.
-		if ($really_previewing == true || isset($_REQUEST['xml']))
+		if (($really_previewing == true || isset($_REQUEST['xml'])) && !isset($_POST['id_draft']))
 		{
 			// Set up the preview message and subject and censor them...
 			$context['preview_message'] = $form_message;
@@ -638,9 +638,8 @@ function Post($post_errors = array())
 			)
 		);
 		// The message they were trying to edit was most likely deleted.
-		//@todo Change this error message?
 		if ($smcFunc['db_num_rows']($request) == 0)
-			fatal_lang_error('no_board', false);
+			fatal_lang_error('no_message', false);
 		$row = $smcFunc['db_fetch_assoc']($request);
 
 		$attachment_stuff = array($row);
@@ -1051,6 +1050,17 @@ function Post($post_errors = array())
 	$context['subject'] = addcslashes($form_subject, '"');
 	$context['message'] = str_replace(array('"', '<', '>', '&nbsp;'), array('&quot;', '&lt;', '&gt;', ' '), $form_message);
 
+	// Are post drafts enabled?
+	$context['drafts_save'] = !empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_post_enabled']) && allowedTo('post_draft');
+	$context['drafts_autosave'] = !empty($context['drafts_save']) && !empty($modSettings['drafts_autosave_enabled']) && allowedTo('post_autosave_draft');
+
+	// Build a list of drafts that they can load in to the editor
+	if (!empty($context['drafts_save']))
+	{
+		require_once($sourcedir . '/Drafts.php');
+		ShowDrafts($user_info['id'], $topic);
+	}
+
 	// Needed for the editor and message icons.
 	require_once($sourcedir . '/Subs-Editor.php');
 
@@ -1229,6 +1239,10 @@ function Post2()
 	require_once($sourcedir . '/Subs-Post.php');
 	loadLanguage('Post');
 
+	// Drafts enabled?
+	if (!empty($modSettings['drafts_enabled']) && isset($_POST['save_draft']))
+		require_once($sourcedir . '/Drafts.php');
+
 	// First check to see if they are trying to delete any current attachments.
 	if (isset($_POST['attach_del']))
 	{
@@ -1528,6 +1542,13 @@ function Post2()
 		if (isset($_POST['sticky']) && (empty($modSettings['enableStickyTopics']) || $_POST['sticky'] == $topic_info['is_sticky'] || !allowedTo('make_sticky')))
 			unset($_POST['sticky']);
 
+		// If drafts are enabled, then pass this off
+		if (!empty($modSettings['drafts_enabled']) && isset($_POST['save_draft']))
+		{
+			SaveDraft($post_errors);
+			return Post();
+		}
+
 		// If the number of replies has changed, if the setting is enabled, go back to Post() - which handles the error.
 		if (empty($options['no_new_reply_warning']) && isset($_POST['last_msg']) && $topic_info['id_last_msg'] > $_POST['last_msg'])
 		{
@@ -1566,6 +1587,13 @@ function Post2()
 		if (isset($_POST['sticky']) && (empty($modSettings['enableStickyTopics']) || empty($_POST['sticky']) || !allowedTo('make_sticky')))
 			unset($_POST['sticky']);
 
+		// Saving your new topic as a draft first?
+		if (!empty($modSettings['drafts_enabled']) && isset($_POST['save_draft']))
+		{
+			SaveDraft($post_errors);
+			return Post();
+		}
+
 		$posterIsGuest = $user_info['is_guest'];
 	}
 	// Modifying an existing message?
@@ -1642,6 +1670,13 @@ function Post2()
 				$moderationAction = true;
 		}
 
+		// If drafts are enabled, then lets send this off to save
+		if (!empty($modSettings['drafts_enabled']) && isset($_POST['save_draft']))
+		{
+			SaveDraft($post_errors);
+			return Post();
+		}
+
 		$posterIsGuest = empty($row['id_member']);
 
 		// Can they approve it?

+ 11 - 1
Sources/Profile.php

@@ -30,7 +30,7 @@ function ModifyProfile($post_errors = array())
 
 	// Don't reload this as we may have processed error strings.
 	if (empty($post_errors))
-		loadLanguage('Profile');
+		loadLanguage('Profile+Drafts');
 	loadTemplate('Profile');
 
 	require_once($sourcedir . '/Subs-Menu.php');
@@ -116,6 +116,16 @@ function ModifyProfile($post_errors = array())
 						'any' => 'profile_view_any',
 					),
 				),
+				'showdrafts' => array(
+					'label' => $txt['drafts_show'],
+					'file' => 'Drafts.php',
+					'function' => 'showProfileDrafts',
+					'enabled' => !empty($modSettings['drafts_enabled']) && $context['user']['is_owner'],
+					'permission' => array(
+						'own' => 'profile_view_own',
+						'any' =>  array(),
+					),
+				),
 				'permissions' => array(
 					'label' => $txt['showPermissions'],
 					'file' => 'Profile-View.php',

+ 41 - 1
Sources/ScheduledTasks.php

@@ -2,7 +2,7 @@
 
 /**
  * This file is automatically called and handles all manner of scheduled things.
- * 
+ *
  * Simple Machines Forum (SMF)
  *
  * @package SMF
@@ -1720,4 +1720,44 @@ function scheduled_remove_topic_redirect()
 	return true;
 }
 
+/**
+ * Check for old drafts and remove them
+ */
+function scheduled_remove_old_drafts()
+{
+	global $smcFunc, $sourcedir, $modSettings;
+
+	if (empty($modSettings['drafts_keep_days']))
+		return true;
+
+	// init
+	$drafts= array();
+
+	// We need this for lanaguage items
+	loadEssentialThemeData();
+
+	// Find all of the old drafts
+	$request = $smcFunc['db_query']('', '
+		SELECT id_draft
+		FROM {db_prefix}user_drafts
+		WHERE poster_time <= {int:poster_time_old}',
+		array(
+			'poster_time_old' => time() - (86400 * $modSettings['drafts_keep_days']),
+		)
+	);
+
+	while ($row = $smcFunc['db_fetch_row']($request))
+		$drafts[] = $row[0];
+	$smcFunc['db_free_result']($request);
+
+	// If we have old one, remove them
+	if (count($drafts) > 0)
+	{
+		require_once($sourcedir . '/Drafts.php');
+		DeleteDraft($drafts, false);
+	}
+
+	return true;
+}
+
 ?>

+ 14 - 16
Sources/Subs-Editor.php

@@ -1332,7 +1332,7 @@ function loadLocale()
 	else
 		@ob_start();
 
-	// If we don't have any locale better avoit broken js
+	// If we don't have any locale better avoid broken js
 	if (empty($txt['lang_locale']))
 		die();
 
@@ -1470,27 +1470,25 @@ function create_control_richedit($editorOptions)
 
 		// This really has some WYSIWYG stuff.
 		loadTemplate('GenericControls', isBrowser('ie') ? 'editor_ie' : 'editor');
-		$context['html_headers'] .= '
-		<script type="text/javascript"><!-- // --><![CDATA[
-			var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
-			var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
-			var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
-			var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';
-		// ]]></script>
-		<script type="text/javascript" src="' . $settings['default_theme_url'] . '/scripts/editor.js?alp21"></script>
-		<link rel="stylesheet" href="' . $settings['default_theme_url'] . '/css/jquery.sceditor.css" type="text/css" media="all" />
-		<script type="text/javascript" src="' . $settings['default_theme_url'] . '/scripts/jquery.sceditor.js"></script>
-		<script type="text/javascript" src="' . $settings['default_theme_url'] . '/scripts/jquery.sceditor.bbcode.js"></script>';
 
+		// Css and JS make the editor go round
+		loadCSSFile( $settings['default_theme_url'] . '/css/jquery.sceditor.css', array());
+		loadJavascriptFile($settings['default_theme_url'] . '/scripts/editor.js?alp21', array(), 'editor.js');
+		loadJavascriptFile($settings['default_theme_url'] . '/scripts/jquery.sceditor.js?alp21', array());
+		loadJavascriptFile($settings['default_theme_url'] . '/scripts/jquery.sceditor.bbcode.js?alp21', array());
+		addInlineJavascript('	
+		var smf_smileys_url = \'' . $settings['smileys_url'] . '\';
+		var bbc_quote_from = \'' . addcslashes($txt['quote_from'], "'") . '\';
+		var bbc_quote = \'' . addcslashes($txt['quote'], "'") . '\';
+		var bbc_search_on = \'' . addcslashes($txt['search_on'], "'") . '\';');
+		// editor language file
 		if (!empty($txt['lang_locale']) && $txt['lang_locale'] != 'en_US')
-			$context['html_headers'] .= '
-		<script type="text/javascript" src="' . $scripturl . '?action=loadeditorlocale"></script>';
+			loadJavascriptFile($scripturl . '?action=loadeditorlocale', array(), 'sceditor_language');
 
 		$context['show_spellchecking'] = !empty($modSettings['enableSpellChecking']) && function_exists('pspell_new');
 		if ($context['show_spellchecking'])
 		{
-			$context['html_headers'] .= '
-		<script type="text/javascript" src="' . $settings['default_theme_url'] . '/scripts/spellcheck.js?alp21"></script>';
+			loadJavascriptFile($settings['default_theme_url'] . '/scripts/spellcheck.js?alp21', array());
 
 			// Some hidden information is needed in order to make the spell checking work.
 			if (!isset($_REQUEST['xml']))

+ 9 - 0
Sources/Subs-Members.php

@@ -208,6 +208,15 @@ function deleteMembers($users, $check_not_admin = false)
 			'users' => $users,
 		)
 	);
+	
+	// Delete any drafts...
+	$smcFunc['db_query']('', '
+		DELETE FROM {db_prefix}user_drafts
+		WHERE id_member IN ({array_int:users})',
+		array(
+			'users' => $users,
+		)
+	);
 
 	// Delete the logs...
 	$smcFunc['db_query']('', '

+ 8 - 5
Sources/Subs-Post.php

@@ -232,6 +232,11 @@ function preparsecode(&$message, $previewing = false)
 			// Close any remaining table tags.
 			foreach ($table_array as $tag)
 				$parts[$i] .= '[/' . $tag . ']';
+			
+			// Remove empty bbc from the sections outside the code tags
+			$parts[$i] = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $parts[$i]);
+			$parts[$i] = preg_replace('~\[quote\]\s*\[/quote\]~', '', $parts[$i]);
+			$parts[$i] = preg_replace('~\[color=(?:#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\(\d{1,3}, ?\d{1,3}, ?\d{1,3}\))\]\s*\[/color\]~', '', $parts[$i]);
 		}
 	}
 
@@ -243,10 +248,6 @@ function preparsecode(&$message, $previewing = false)
 
 	// Now let's quickly clean up things that will slow our parser (which are common in posted code.)
 	$message = strtr($message, array('[]' => '&#91;]', '[&#039;' => '&#91;&#039;'));
-
-	// Remove empty bbc.
-	$message = preg_replace('~\[[bisu]\]\s*\[/[bisu]\]~', '', $message);
-	$message = preg_replace('~\[quote\]\s*\[/quote\]~', '', $message);
 }
 
 /**
@@ -827,8 +828,10 @@ function sendpm($recipients, $subject, $message, $store_outbox = false, $from =
 
 	// This is the one that will go in their inbox.
 	$htmlmessage = $smcFunc['htmlspecialchars']($message, ENT_QUOTES);
-	$htmlsubject = $smcFunc['htmlspecialchars']($subject);
 	preparsecode($htmlmessage);
+	$htmlsubject = strtr($smcFunc['htmlspecialchars']($subject), array("\r" => '', "\n" => '', "\t" => ''));
+	if ($smcFunc['strlen']($htmlsubject) > 100)
+		$htmlsubject = $smcFunc['substr']($htmlsubject, 0, 100);
 
 	// Integrated PMs
 	call_integration_hook('integrate_personal_message', array(&$recipients, &$from['username'], &$subject, &$message));

+ 23 - 31
Sources/Subs.php

@@ -841,7 +841,7 @@ function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = arra
 	global $txt, $scripturl, $context, $modSettings, $user_info, $smcFunc;
 	static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
 	static $disabled;
-
+	
 	// Don't waste cycles
 	if ($message === '')
 		return '';
@@ -1071,12 +1071,7 @@ function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = arra
 
 						// Fix the PHP code stuff...
 						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));
-
-						// Older browsers are annoying, aren\'t they?
-						if ($context[\'browser\'][\'is_ie4\'] || $context[\'browser\'][\'is_ie5\'] || $context[\'browser\'][\'is_ie5.5\'])
-							$data = str_replace("\t", "<pre style=\"display: inline;\">\t</pre>", $data);
-						else
-							$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
+						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
 
 						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
 						if ($context[\'browser\'][\'is_opera\'])
@@ -1113,12 +1108,7 @@ function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = arra
 
 						// Fix the PHP code stuff...
 						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode(\'\', $php_parts));
-
-						// Older browsers are annoying, aren\'t they?
-						if ($context[\'browser\'][\'is_ie4\'] || $context[\'browser\'][\'is_ie5\'] || $context[\'browser\'][\'is_ie5.5\'])
-							$data[0] = str_replace("\t", "<pre style=\"display: inline;\">\t</pre>", $data[0]);
-						else
-							$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
+						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
 
 						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
 						if ($context[\'browser\'][\'is_opera\'])
@@ -3232,7 +3222,11 @@ function template_footer()
 }
 
 /**
- * Output the Javascript files (messed up tabbing in this function is to make the HTML source look good)
+ * Output the Javascript files 
+ * 	- tabbing in this function is to make the HTML source look good proper
+ *  - if defered is set function will output all JS (source & inline) set to load at page end
+ *
+ * @param bool $do_defered = false
  */
 function template_javascript($do_defered = false)
 {
@@ -3241,7 +3235,7 @@ function template_javascript($do_defered = false)
 	// Use this hook to minify/optimize Javascript files and vars
 	call_integration_hook('pre_javascript_output');
 	
-	// Javascript variables.
+	// Ouput the declared Javascript variables.
 	if (!empty($context['javascript_vars']) && !$do_defered)
 	{
 		echo '
@@ -3255,15 +3249,15 @@ function template_javascript($do_defered = false)
 	// ]]></script>';
 	}
 
-	// Javascript files
-	foreach ($context['javascript_files'] as $id => $file)
+	// While we have Javascript files to place in the template
+	foreach ($context['javascript_files'] as $id => $js_file)
 	{
-		if ((!$do_defered && empty($file['options']['defer'])) || ($do_defered && !empty($file['options']['defer'])))
+		if ((!$do_defered && empty($js_file['options']['defer'])) || ($do_defered && !empty($js_file['options']['defer'])))
 			echo '
-	<script type="text/javascript" src="', $file['filename'], '" id="', $id,'"' , !empty($file['options']['async']) ? ' async="async"' : '' ,'></script>';
+	<script type="text/javascript" src="', $js_file['filename'], '" id="', $id,'"' , !empty($js_file['options']['async']) ? ' async="async"' : '' ,'></script>';
 	
-		// If this was JQuery being loaded and we are set to 'auto' load it, add the inline JS stuff here
-		if($id == 'jquery' && (!isset($modSettings['jquery_source']) || !in_array($modSettings['jquery_source'],array('local', 'cdn'))))
+		// If we are loading JQuery and we are set to 'auto' load, put in our remote success or load local check
+		if ($id == 'jquery' && (!isset($modSettings['jquery_source']) || !in_array($modSettings['jquery_source'],array('local', 'cdn'))))
 		echo '
 	<script type="text/javascript"><!-- // --><![CDATA[
 		window.jQuery || document.write(\'<script src="' . $settings['default_theme_url'] . '/scripts/jquery-1.7.1.min.js"><\/script>\');
@@ -3274,27 +3268,25 @@ function template_javascript($do_defered = false)
 	// Inline JavaScript - Actually useful some times!
 	if (!empty($context['javascript_inline']))
 	{
-		if(!empty($context['javascript_inline']['defer']) && $do_defered)
+		if (!empty($context['javascript_inline']['defer']) && $do_defered)
 		{
 			echo '
-<script type="text/javascript"><!-- // --><![CDATA[
-	';
+<script type="text/javascript"><!-- // --><![CDATA[';
 
-			foreach ($context['javascript_inline']['defer'] as $code)
-				echo $code;
+			foreach ($context['javascript_inline']['defer'] as $js_code)
+				echo $js_code;
 					
 			echo'
 // ]]></script>';
 		}
 
-		if(!empty($context['javascript_inline']['standard']) && !$do_defered)
+		if (!empty($context['javascript_inline']['standard']) && !$do_defered)
 		{
 			echo '
-	<script type="text/javascript"><!-- // --><![CDATA[
-		';
+	<script type="text/javascript"><!-- // --><![CDATA[';
 
-			foreach ($context['javascript_inline']['standard'] as $code)
-				echo $code;
+			foreach ($context['javascript_inline']['standard'] as $js_code)
+				echo $js_code;
 					
 			echo'
 	// ]]></script>';

+ 0 - 4
Themes/default/Admin.template.php

@@ -574,7 +574,6 @@ function template_view_versions()
 	   file categories. (sources, languages, and templates.) */
 	echo '
 		<script type="text/javascript" src="', $scripturl, '?action=viewsmfile;filename=detailed-version.js"></script>
-		<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/admin.js?alp21"></script>
 		<script type="text/javascript"><!-- // --><![CDATA[
 			var oViewVersions = new smf_ViewVersions({
 				aKnownLanguages: [
@@ -625,7 +624,6 @@ function template_edit_censored()
 					<div id="moreCensoredWords"></div><div style="margin-top: 1ex; display: none;" id="moreCensoredWords_link">
 						<a class="button_link" style="float: left" href="#;" onclick="addNewWord(); return false;">', $txt['censor_clickadd'], '</a><br />
 					</div>
-					<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/admin.js?alp21"></script>
 					<script type="text/javascript"><!-- // --><![CDATA[
 						document.getElementById("moreCensoredWords_link").style.display = "";
 					// ]]></script>
@@ -1459,8 +1457,6 @@ function template_callback_question_answer_list()
 		<dt id="add_more_question_placeholder" style="display: none;"></dt><dd></dd>
 		<dt id="add_more_link_div" style="display: none;">
 			<a href="#" onclick="addAnotherQuestion(); return false;">&#171; ', $txt['setup_verification_add_more'], ' &#187;</a>
-			<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/admin.js?alp21"></script>
-
 		</dt><dd></dd>';
 
 	// The javascript needs to go at the end but we'll put it in this template for looks.

+ 3 - 3
Themes/default/BoardIndex.template.php

@@ -252,7 +252,7 @@ function template_info_center()
 	<div class="roundframe" id="info_center">
 		<div class="cat_bar">
 			<h3 class="catbg">
-				<img class="icon" id="upshrink_ic" src="', $settings['images_url'], '/collapse.png" alt="*" title="', $txt['upshrink_description'], '" style="display: none;" />
+				<img class="icon" id="upshrink_ic" src="', $settings['images_url'], '/collapse.png" alt="*" title="', $txt['hide'], '" style="display: none;" />
 				<a href="#" id="upshrink_link">', sprintf($txt['info_center_title'], $context['forum_name_html_safe']), '</a>
 			</h3>
 		</div>
@@ -441,9 +441,9 @@ function template_info_center()
 				{
 					sId: \'upshrink_ic\',
 					srcExpanded: smf_images_url + \'/collapse.png\',
-					altExpanded: ', JavaScriptEscape($txt['upshrink_description']), ',
+					altExpanded: ', JavaScriptEscape($txt['hide']), ',
 					srcCollapsed: smf_images_url + \'/expand.png\',
-					altCollapsed: ', JavaScriptEscape($txt['upshrink_description']), '
+					altCollapsed: ', JavaScriptEscape($txt['show']), '
 				}
 			],
 			aSwapLinks: [

+ 24 - 2
Themes/default/Display.template.php

@@ -768,6 +768,15 @@ function template_main()
 		if ($context['show_spellchecking'])
 			echo '
 								<input type="button" value="', $txt['spell_check'], '" onclick="spellCheck(\'postmodify\', \'message\');" tabindex="', $context['tabindex']++, '" class="button_submit" />';
+								
+		if (($context['drafts_save']) && !empty($options['drafts_show_saved_enabled']))
+			echo '	
+								<input type="submit" name="save_draft" value="', $txt['draft_save'], '" onclick="return confirm(' . JavaScriptEscape($txt['draft_save_note']) . ') && submitThisOnce(this);" accesskey="d" tabindex="', $context['tabindex']++, '" class="button_submit" />
+								<input type="hidden" id="id_draft" name="id_draft" value="', empty($context['id_draft']) ? 0 : $context['id_draft'], '" />';
+								
+		if (!empty($context['drafts_autosave']) && !empty($options['drafts_autosave_enabled']))
+			echo '
+								<div class="clear righttext padding"><span id="throbber" style="display:none"><img src="' . $settings['images_url'] . '/loading_sm.gif" alt="" class="centericon" />&nbsp;</span><span id="draft_lastautosave" ></span></div>';
 
 		echo '
 							</div>
@@ -779,6 +788,21 @@ function template_main()
 	else
 		echo '
 		<br class="clear" />';
+		
+	if (!empty($context['drafts_autosave']) && !empty($options['drafts_autosave_enabled']))
+		echo '
+			<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/drafts.js?alp21"></script>
+			<script type="text/javascript"><!-- // --><![CDATA[
+				var oDraftAutoSave = new smf_DraftAutoSave({
+					sSelf: \'oDraftAutoSave\',
+					sLastNote: \'draft_lastautosave\',
+					sLastID: \'id_draft\',
+					sSceditorID: \'', $context['post_box_name']. '\',
+					sType: \'', !empty($options['display_quick_reply']) && $options['display_quick_reply'] > 2 ? 'quick' : 'quick', '\',
+					iBoard: ', (empty($context['current_board']) ? 0 : $context['current_board']), ',
+					iFreq: ', (empty($modSettings['masterAutoSaveDraftsDelay']) ? 60000 : $modSettings['masterAutoSaveDraftsDelay'] * 1000), '
+				});
+			// ]]></script>';
 
 	if ($context['show_spellchecking'])
 		echo '
@@ -893,10 +917,8 @@ function template_main()
 					}';
 
 	if (!empty($ignoredMsgs))
-	{
 		echo '
 					ignore_toggles([', implode(', ', $ignoredMsgs), '], ', JavaScriptEscape($txt['show_ignore_user_post']), ');';
-	}
 
 	echo '
 				// ]]></script>';

+ 60 - 2
Themes/default/GenericControls.template.php

@@ -34,7 +34,7 @@ function template_control_richedit($editor_id, $smileyContainer = null, $bbcCont
 				$("#', $editor_id, '").sceditorBBCodePlugin({
 					style: "', $settings['default_theme_url'], '/css/jquery.sceditor.default.css",
 					emoticonsCompat: true,
-					supportedWysiwyg: ((is_ie5up && !is_ie50) || is_ff || is_opera95up || is_safari || is_chrome),',
+					supportedWysiwyg: (is_ie || is_ff || is_opera95up || is_safari || is_chrome),',
 					!empty($editor_context['locale']) ? '
 					locale: \'' . $editor_context['locale'] . '\',' : '', '
 					colors: "black,red,yellow,pink,green,orange,purple,blue,beige,brown,teal,navy,maroon,limegreen,white"';
@@ -102,7 +102,7 @@ function template_control_richedit($editor_id, $smileyContainer = null, $bbcCont
 				$(".sceditor-container").width("100%").height("100%");', 
 				$editor_context['rich_active'] ? '' : '
 				$("#' . $editor_id . '").data("sceditor").setTextMode();', '
-				if (!((is_ie5up && !is_ie50) || is_ff || is_opera95up || is_safari || is_chrome))
+				if (!(is_ie || is_ff || is_opera95up || is_safari || is_chrome))
 				{
 					$("#' . $editor_id . '").data("sceditor").setTextMode();
 					$(".sceditor-button-source").hide();
@@ -141,6 +141,64 @@ function template_control_richedit_buttons($editor_id)
 	if ($context['show_spellchecking'])
 		echo '
 		<input type="button" value="', $txt['spell_check'], '" tabindex="', $context['tabindex']++, '" onclick="oEditorHandle_', $editor_id, '.spellCheckStart();" class="button_submit" />';
+		
+	if (!empty($context['drafts_save']))
+	{
+		// Show the save draft button
+		echo '
+		<input type="submit" name="save_draft" value="', $txt['draft_save'], '" tabindex="', $context['tabindex']++, '" onclick="return confirm(' . JavaScriptEscape($txt['draft_save_note']) . ') && submitThisOnce(this);" accesskey="d" class="button_submit" />
+		<input type="hidden" id="id_draft" name="id_draft" value="', empty($context['id_draft']) ? 0 : $context['id_draft'], '" />';
+		
+		// Start an instance of the auto saver if its enabled
+		if (!empty($context['drafts_autosave']) && !empty($options['drafts_autosave_enabled']))
+			echo '
+		<br />
+		<span class="righttext padding" style="display: block">
+			<span id="throbber" style="display:none"><img src="' . $settings['images_url'] . '/loading_sm.gif" alt="" class="centericon" />&nbsp;</span>
+			<span id="draft_lastautosave" ></span>
+		</span>
+		<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/drafts.js?alp21"></script>
+		<script type="text/javascript"><!-- // --><![CDATA[
+			var oDraftAutoSave = new smf_DraftAutoSave({
+				sSelf: \'oDraftAutoSave\',
+				sLastNote: \'draft_lastautosave\',
+				sLastID: \'id_draft\',
+				sSceditorID: \'', $editor_id, '\',
+				sType: \'post\',
+				iBoard: ', (empty($context['current_board']) ? 0 : $context['current_board']), ',
+				iFreq: ', (empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000), '
+			});
+		// ]]></script>';
+	}
+
+	if (!empty($context['drafts_pm_save']))
+	{
+		// The PM draft save button
+		echo '
+		<input type="submit" name="save_draft" value="', $txt['draft_save'], '" tabindex="', $context['tabindex']++, '" onclick="submitThisOnce(this);" accesskey="d" class="button_submit" />
+		<input type="hidden" id="id_pm_draft" name="id_pm_draft" value="', empty($context['id_pm_draft']) ? 0 : $context['id_pm_draft'], '" />';
+
+		// Load in the PM autosaver if its enabled and the user wants to use it
+		if (!empty($context['drafts_autosave']) && !empty($options['drafts_autosave_enabled']))
+			echo '
+		<span class="righttext padding" style="display: block">
+			<span id="throbber" style="display:none"><img src="' . $settings['images_url'] . '/loading_sm.gif" alt="" class="centericon" />&nbsp;</span>
+			<span id="draft_lastautosave" ></span>
+		</span>
+		<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/drafts.js?alp21"></script>
+		<script type="text/javascript"><!-- // --><![CDATA[
+			var oDraftAutoSave = new smf_DraftAutoSave({
+				sSelf: \'oDraftAutoSave\',
+				sLastNote: \'draft_lastautosave\',
+				sLastID: \'id_pm_draft\',
+				sSceditorID: \'', $editor_id, '\',
+				sType: \'post\',
+				bPM: true,
+				iBoard: 0,
+				iFreq: ', (empty($modSettings['drafts_autosave_frequency']) ? 60000 : $modSettings['drafts_autosave_frequency'] * 1000), '
+			});
+		// ]]></script>';
+	}
 }
 
 // What's this, verification?!

+ 18 - 3
Themes/default/ManageMaintenance.template.php

@@ -254,10 +254,11 @@ function template_maintain_members()
 		{
 			membersSwap = !membersSwap;
 			var membersForm = document.getElementById(\'membersForm\');
-
+			
+			$("#membersPanel").slideToggle(300);
+			
 			document.getElementById("membersIcon").src = smf_images_url + (membersSwap ? "/selected_open.png" : "/selected.png");
 			setInnerHTML(document.getElementById("membersText"), membersSwap ? "', $txt['maintain_members_choose'], '" : "', $txt['maintain_members_all'], '");
-			document.getElementById("membersPanel").style.display = (membersSwap ? "block" : "none");
 
 			for (var i = 0; i < membersForm.length; i++)
 			{
@@ -431,7 +432,7 @@ function template_maintain_topics()
 				setInnerHTML(document.getElementById("rotText"), rotSwap ? ', JavaScriptEscape($txt['maintain_old_choose']), ' : ', JavaScriptEscape($txt['maintain_old_all']), ');
 
 				// Toggle panel
-				document.getElementById("rotPanel").style.display = !rotSwap ? "none" : "";
+				$("#rotPanel").slideToggle(300);
 
 				// Toggle checkboxes
 				var rotPanel = document.getElementById(\'rotPanel\');
@@ -513,6 +514,20 @@ function template_maintain_topics()
 				</form>
 			</div>
 		</div>
+
+		<div class="cat_bar">
+			<h3 class="catbg">', $txt['maintain_old_drafts'], '</h3>
+		</div>
+		<div class="windowbg">
+			<div class="content">
+				<form action="', $scripturl, '?action=admin;area=maintain;sa=topics;activity=olddrafts" method="post" accept-charset="', $context['character_set'], '">
+					<p>', $txt['maintain_old_drafts_days'], '&nbsp;<input type="text" name="draftdays" value="', (!empty($modSettings['drafts_keep_days']) ? $modSettings['drafts_keep_days'] : 30), '" size="3" />&nbsp;', $txt['days_word'], '</p>
+					<input type="submit" value="', $txt['maintain_old_remove'], '" onclick="return confirm(\'', $txt['maintain_old_drafts_confirm'], '\');" class="button_submit" />
+					<input type="hidden" name="', $context['session_var'], '" value="', $context['session_id'], '" />
+					<input type="hidden" name="', $context['admin-maint_token_var'], '" value="', $context['admin-maint_token'], '" />
+				</form>
+			</div>
+		</div>
 		<div class="cat_bar">
 			<h3 class="catbg">', $txt['move_topics_maintenance'], '</h3>
 		</div>

+ 121 - 1
Themes/default/PersonalMessage.template.php

@@ -975,6 +975,12 @@ function template_send()
 					</dl>
 				</div>';
 
+	if (!empty($modSettings['drafts_pm_enabled']))
+		echo '			
+				<div id="draft_section" class="infobox"', isset($context['draft_saved']) ? '' : ' style="display: none;"', '>',
+					sprintf($txt['draft_pm_saved'], $scripturl . '?action=pm;sa=showpmdrafts'), '
+				</div>';
+				
 	echo '
 				<dl id="post_header">';
 
@@ -1016,7 +1022,7 @@ function template_send()
 						<span', (isset($context['post_error']['no_subject']) ? ' class="error"' : ''), ' id="caption_subject">', $txt['subject'], ':</span>
 					</dt>
 					<dd id="pm_subject">
-						<input type="text" name="subject" value="', $context['subject'], '" tabindex="', $context['tabindex']++, '" size="60" maxlength="60"',isset($context['post_error']['no_subject']) ? ' class="error"' : ' class="input_text"', '/>
+						<input type="text" name="subject" value="', $context['subject'], '" tabindex="', $context['tabindex']++, '" size="80" maxlength="80"',isset($context['post_error']['no_subject']) ? ' class="error"' : ' class="input_text"', '/>
 					</dd>
 				</dl><hr class="clear" />';
 
@@ -1068,6 +1074,30 @@ function template_send()
 			</div>
 		</div>
 	</form>';
+	
+	// If the admin enabled the pm drafts feature, show a draft selection box
+	if (!empty($modSettings['drafts_enabled']) && !empty($context['drafts_pm_save']) && !empty($context['drafts']) && !empty($options['drafts_show_saved_enabled']))
+	{
+		echo '
+			<br />
+			<div id="postDraftOptionsHeader" class="title_bar">
+				<h4 class="titlebg">
+					<img id="postDraftExpand" class="panel_toggle" style="display: none;" src="', $settings['images_url'], '/collapse.png" alt="-" /> <strong><a href="#" id="postDraftExpandLink">', $txt['draft_load'], '</a></strong>
+				</h4>
+			</div>
+			<div id="postDraftOptions" class="load_drafts padding">
+				<dl class="settings">
+					<dt><strong>', $txt['subject'], '</strong></dt>
+					<dd><strong>', $txt['draft_saved_on'], '</strong></dd>';
+				
+		foreach ($context['drafts'] as $draft)
+			echo '
+					<dt>', $draft['link'], '</dt>
+					<dd>', $draft['poster_time'], '</dd>';
+		echo '
+				</dl>
+			</div>';
+	}
 
 	echo '
 		<script type="text/javascript"><!-- // --><![CDATA[';
@@ -1175,6 +1205,33 @@ function template_send()
 				}
 				location.hash = \'#\' + \'preview_section\';
 			}';
+			
+	// Code for showing and hiding drafts
+	if (!empty($context['drafts']))
+		echo '
+			var oSwapDraftOptions = new smc_Toggle({
+				bToggleEnabled: true,
+				bCurrentlyCollapsed: true,
+				aSwappableContainers: [
+					\'postDraftOptions\',
+				],
+				aSwapImages: [
+					{
+						sId: \'postDraftExpand\',
+						srcExpanded: smf_images_url + \'/collapse.png\',
+						altExpanded: \'-\',
+						srcCollapsed: smf_images_url + \'/expand.png\',
+						altCollapsed: \'+\'
+					}
+				],
+				aSwapLinks: [
+					{
+						sId: \'postDraftExpandLink\',
+						msgExpanded: ', JavaScriptEscape($txt['draft_hide']), ',
+						msgCollapsed: ', JavaScriptEscape($txt['draft_load']), '
+					}
+				]
+			});';
 
 	echo '
 		// ]]></script>';
@@ -1822,4 +1879,67 @@ function template_add_rule()
 		// ]]></script>';
 }
 
+// Template for showing all the PM drafts of the user.
+function template_showPMDrafts()
+{
+	global $context, $settings, $options, $scripturl, $modSettings, $txt;
+	
+	echo '
+		<div class="cat_bar">
+			<h3 class="catbg">
+				<img src="', $settings['images_url'], '/message_sm.png" alt="" class="icon" />
+					', $txt['drafts_show'], '
+			</h3>
+		</div>
+		<div class="pagesection">
+			<span>', $txt['pages'], ': ', $context['page_index'], '</span>
+		</div>';
+
+	// Button shortcuts
+	$edit_button = create_button('modify_inline.png', 'draft_edit', 'draft_edit', 'class="centericon"');
+	$remove_button = create_button('delete.png', 'draft_delete', 'draft_delete', 'class="centericon"');
+
+	// For every draft to be displayed, give it its own div, and show the important details of the draft.
+	foreach ($context['drafts'] as $draft)
+	{
+		echo '
+		<div class="topic">
+			<div class="', $draft['alternate'] == 0 ? 'windowbg2' : 'windowbg', ' core_posts">
+				<div class="content">
+					<div class="counter">', $draft['counter'], '</div>
+					<div class="topic_details">
+						<h5><strong>', $draft['subject'], '</strong>&nbsp;';
+
+		echo '
+						</h5>
+						<span class="smalltext">&#171;&nbsp;<strong>', $txt['draft_saved_on'], ':</strong> ', sprintf($txt['draft_days_ago'], $draft['age']), (!empty($draft['remaining']) ? ', ' . sprintf($txt['draft_retain'], $draft['remaining']) : ''), '&#187;</span><br />
+						<span class="smalltext">&#171;&nbsp;<strong>', $txt['to'], ':</strong> ', implode(', ', $draft['recipients']['to']), '&nbsp;&#187;</span><br />
+						<span class="smalltext">&#171;&nbsp;<strong>', $txt['pm_bcc'], ':</strong> ', implode(', ', $draft['recipients']['bcc']), '&nbsp;&#187;</span>
+					</div>
+					<div class="list_posts">
+						', $draft['body'], '
+					</div>
+				
+					<ul class="reset smalltext quickbuttons">
+						<li><a href="', $scripturl, '?action=pm;sa=showpmdrafts;id_draft=', $draft['id_draft'], ';', $context['session_var'], '=', $context['session_id'], '"  class="reply_button"><span>', $txt['draft_edit'], '</span></a></li>
+						<li><a href="', $scripturl, '?action=pm;sa=showpmdrafts;delete=', $draft['id_draft'], ';', $context['session_var'], '=', $context['session_id'], '" onclick="return confirm(\'', $txt['draft_remove'], '?\');" class="remove_button"><span>', $txt['draft_delete'], '</span></a></li>
+					</ul>
+				</div>
+			</div>
+		</div>';
+	}
+
+	// No drafts? Just show an informative message.
+	if (empty($context['drafts']))
+		echo '
+		<div class="tborder windowbg2 padding centertext">
+			', $txt['draft_none'], '
+		</div>';
+
+	// Show page numbers.
+	echo '
+		<div class="pagesection" style="margin-bottom: 0;">
+			<span>', $txt['pages'], ': ', $context['page_index'], '</span>
+		</div>';
+}
 ?>

+ 66 - 5
Themes/default/Post.template.php

@@ -123,7 +123,13 @@ function template_main()
 					<p class="information"', $context['locked'] ? '' : ' style="display: none"', ' id="lock_warning">
 						', $txt['topic_locked_no_reply'], '
 					</p>';
-
+	
+	if (!empty($modSettings['drafts_post_enabled']))
+		echo '
+				<div id="draft_section" class="infobox"', isset($context['draft_saved']) ? '' : ' style="display: none;"', '>',
+					sprintf($txt['draft_saved'], $scripturl . '?action=profile;u=' . $context['user']['id'] . ';area=showdrafts'), '
+				</div>';
+				
 	// The post header... important stuff
 	echo '
 					<dl id="post_header">';
@@ -172,7 +178,8 @@ function template_main()
 							</select>
 							<img src="', $context['icon_url'], '" name="icons" hspace="15" alt="" />
 						</dd>
-					</dl><hr class="clear" />';
+					</dl>
+					<hr class="clear" />';
 
 	// Are you posting a calendar event?
 	if ($context['make_event'])
@@ -344,6 +351,7 @@ function template_main()
 					</div>';
 	}
 
+	// Show the actual posting area...
 	echo '
 					', template_control_richedit($context['post_box_name'], 'smileyBox_message', 'bbcBox_message');
 
@@ -476,6 +484,30 @@ function template_main()
 						</dd>
 					</dl>';
 	}
+	
+	// If the admin enabled the drafts feature, show a draft selection box
+	if (!empty($modSettings['drafts_enabled']) && !empty($context['drafts']) && !empty($options['drafts_show_saved_enabled']))
+	{
+		echo '
+			<br />
+			<div id="postDraftOptionsHeader" class="title_bar">
+				<h4 class="titlebg">
+					<img id="postDraftExpand" class="panel_toggle" style="display: none;" src="', $settings['images_url'], '/collapse.png" alt="-" /> <strong><a href="#" id="postDraftExpandLink">', $txt['draft_load'], '</a></strong>
+				</h4>
+			</div>
+			<div id="postDraftOptions" class="load_drafts padding">
+				<dl class="settings">
+					<dt><strong>', $txt['subject'], '</strong></dt>
+					<dd><strong>', $txt['draft_saved_on'], '</strong></dd>';
+				
+		foreach ($context['drafts'] as $draft)
+			echo '
+					<dt>', $draft['link'], '</dt>
+					<dd>', $draft['poster_time'], '</dd>';
+		echo '
+				</dl>
+			</div>';
+	}
 
 	// Is visual verification enabled?
 	if ($context['require_verification'])
@@ -492,8 +524,8 @@ function template_main()
 	// Finally, the submit buttons.
 	echo '
 					<br class="clear_right" />
-					<span class="smalltext" >
-						', isBrowser('is_firefox') ? $txt['shortcuts_firefox'] : $txt['shortcuts'], '
+					<span class="smalltext">
+						', isBrowser('is_firefox') ? ($context['drafts_save'] ? $txt['shortcuts_drafts_firefox'] : $txt['shortcuts_firefox']) : ($context['drafts_save'] ? $txt['shortcuts_drafts'] : $txt['shortcuts']), '
 					</span>
 					<span id="post_confirm_buttons">
 						', template_control_richedit_buttons($context['post_box_name']);
@@ -744,6 +776,33 @@ function template_main()
 					}
 				]
 			});';
+			
+	// Code for showing and hiding drafts
+	if (!empty($context['drafts']))
+		echo '
+			var oSwapDraftOptions = new smc_Toggle({
+				bToggleEnabled: true,
+				bCurrentlyCollapsed: true,
+				aSwappableContainers: [
+					\'postDraftOptions\',
+				],
+				aSwapImages: [
+					{
+						sId: \'postDraftExpand\',
+						srcExpanded: smf_images_url + \'/collapse.png\',
+						altExpanded: \'-\',
+						srcCollapsed: smf_images_url + \'/expand.png\',
+						altCollapsed: \'+\'
+					}
+				],
+				aSwapLinks: [
+					{
+						sId: \'postDraftExpandLink\',
+						msgExpanded: ', JavaScriptEscape($txt['draft_hide']), ',
+						msgCollapsed: ', JavaScriptEscape($txt['draft_load']), '
+					}
+				]
+			});';
 
 	echo '
 		// ]]></script>';
@@ -768,7 +827,9 @@ function template_main()
 			echo '
 				<div class="', $post['alternate'] == 0 ? 'windowbg' : 'windowbg2', ' core_posts">
 				<div class="content" id="msg', $post['id'], '">
-						<h5 class="floatleft"><span>', $txt['posted_by'], '</span>&nbsp;', $post['poster'], '</h5>&nbsp;-&nbsp;', $post['time'];
+					<h5 class="floatleft">
+						<span>', $txt['posted_by'], '</span>&nbsp;', $post['poster'], '
+					</h5>&nbsp;-&nbsp;', $post['time'];
 
 			if ($context['can_quote'])
 			{

+ 111 - 11
Themes/default/Profile.template.php

@@ -104,7 +104,13 @@ function template_summary()
 		echo '
 					<a href="', $scripturl, '?action=pm;sa=send;u=', $context['id_member'], '">', $txt['profile_sendpm_short'], '</a><br />';
 	echo '
-					<a href="', $scripturl, '?action=profile;area=showposts;u=', $context['id_member'], '">', $txt['showPosts'], '</a><br />
+					<a href="', $scripturl, '?action=profile;area=showposts;u=', $context['id_member'], '">', $txt['showPosts'], '</a><br />';
+	
+	if ($context['user']['is_owner'] && !empty($modSettings['drafts_enabled']))
+		echo '
+					<a href="', $scripturl, '?action=profile;area=showdrafts;u=', $context['id_member'], '">', $txt['drafts_show'], '</a><br />';
+	
+	echo '
 					<a href="', $scripturl, '?action=profile;area=statistics;u=', $context['id_member'], '">', $txt['statPanel'], '</a>
 				</p>';
 
@@ -334,6 +340,9 @@ function template_showPosts()
 			<h3 class="catbg">
 				', (!isset($context['attachments']) && empty($context['is_topics']) ? $txt['showMessages'] : (!empty($context['is_topics']) ? $txt['showTopics'] : $txt['showAttachments'])), ' - ', $context['member']['name'], '
 			</h3>
+		</div>
+		<div class="pagesection">
+			<div class="pagelinks">', $context['page_index'], '</div>
 		</div>';
 
 	// Button shortcuts
@@ -399,15 +408,9 @@ function template_showPosts()
 				</div>';
 
 			echo '
-			</div>
-		</div>';
+				</div>
+			</div>';
 		}
-
-		// Show more page numbers.
-		echo '
-		<div class="pagesection" style="margin-bottom: 0;">
-			<span>', $txt['pages'], ': ', $context['page_index'], '</span>
-		</div>';
 	}
 	else
 		template_show_list('attachments');
@@ -423,8 +426,86 @@ function template_showPosts()
 
 		echo '
 			</tbody>
-		</table>
-	</div>';
+		</table>';
+		
+	// Show more page numbers.
+	echo '
+		<div class="pagesection" style="margin-bottom: 0;">
+			<div class="pagelinks">', $context['page_index'], '</div>
+		</div>';
+}
+
+// Template for showing all the drafts of the user.
+function template_showDrafts()
+{
+	global $context, $settings, $options, $scripturl, $modSettings, $txt;
+	
+	echo '
+		<div class="cat_bar">
+			<h3 class="catbg">
+				<span class="ie6_header floatleft"><img src="', $settings['images_url'], '/message_sm.png" alt="" class="icon" />
+					', $txt['drafts_show'], ' - ', $context['member']['name'], '
+				</span>
+			</h3>
+		</div>
+		<div class="pagesection" style="margin-bottom: 0;">
+			<div class="pagelinks">', $context['page_index'], '</div>
+		</div>';
+
+	// Button shortcuts
+	$edit_button = create_button('modify_inline.png', 'draft_edit', 'draft_edit', 'class="centericon"');
+	$remove_button = create_button('delete.png', 'draft_delete', 'draft_delete', 'class="centericon"');
+
+	// For every draft to be displayed, give it its own div, and show the important details of the draft.
+	foreach ($context['drafts'] as $draft)
+	{
+		echo '
+		<div class="topic">
+			<div class="', $draft['alternate'] == 0 ? 'windowbg2' : 'windowbg', ' core_posts">
+				<span class="topslice"><span></span></span>
+				<div class="content">
+					<div class="counter">', $draft['counter'], '</div>
+					<div class="topic_details">
+						<h5><strong><a href="', $scripturl, '?board=', $draft['board']['id'], '.0">', $draft['board']['name'], '</a> / ', $draft['topic']['link'], '</strong> &nbsp; &nbsp;';
+
+		if (!empty($draft['sticky']))
+			echo '<img src="', $settings['images_url'], '/icons/quick_sticky.png" alt="', $txt['sticky_topic'], '" title="', $txt['sticky_topic'], '" />';
+
+		if (!empty($draft['locked']))
+			echo '<img src="', $settings['images_url'], '/icons/quick_lock.png" alt="', $txt['locked_topic'], '" title="', $txt['locked_topic'], '" />';
+
+		echo '
+						</h5>
+						<span class="smalltext">&#171;&nbsp;<strong>', $txt['on'], ':</strong> ', $draft['time'], '&nbsp;&#187;</span>
+					</div>
+					<div class="list_posts">
+						', $draft['body'], '
+					</div>
+				</div>
+				<div class="floatright">
+					<ul class="reset smalltext quickbuttons">
+						<li><a href="', $scripturl, '?action=post;', (empty($draft['topic']['id']) ? 'board=' . $draft['board']['id'] : 'topic=' . $draft['topic']['id']), '.0;id_draft=', $draft['id_draft'], '" class="reply_button"><span>', $txt['draft_edit'], '</span></a></li>
+						<li><a href="', $scripturl, '?action=profile;u=', $context['member']['id'], ';area=showdrafts;delete=', $draft['id_draft'], ';', $context['session_var'], '=', $context['session_id'], '" onclick="return confirm(\'', $txt['draft_remove'], '?\');" class="remove_button"><span>', $txt['draft_delete'], '</span></a></li>
+					</ul>
+				</div>
+				<br class="clear" />
+				<span class="botslice"><span></span></span>
+			</div>
+		</div>';
+	}
+
+	// No drafts? Just show an informative message.
+	if (empty($context['drafts']))
+		echo '
+		<div class="tborder windowbg2 padding centertext">
+			', $txt['draft_none'], '
+		</div>';
+
+	// Show page numbers.
+	echo '
+		<div class="pagesection" style="margin-bottom: 0;">
+			<div class="pagelinks">', $context['page_index'], '</div>
+		</div>';
 }
 
 // Template for showing all the buddies of the current user.
@@ -1576,6 +1657,25 @@ function template_profile_theme_settings()
 									<option value="6"', !empty($context['member']['options']['calendar_start_day']) && $context['member']['options']['calendar_start_day'] == 6 ? ' selected="selected"' : '', '>', $txt['days'][6], '</option>
 								</select>
 							</dd>';
+							
+	if (!empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_autosave_enabled']))
+		echo '
+							<dt>
+								<label for="drafts_autosave_enabled">', $txt['drafts_autosave_enabled'], '</label>
+							</dt>
+							<dd>
+								<input type="hidden" name="default_options[drafts_autosave_enabled]" value="0" />
+								<label for="drafts_autosave_enabled"><input type="checkbox" name="default_options[drafts_autosave_enabled]" id="drafts_autosave_enabled" value="1"', !empty($context['member']['options']['drafts_autosave_enabled']) ? ' checked="checked"' : '', ' class="input_check" /></label>
+							</dd>';
+	if (!empty($modSettings['drafts_enabled']) && !empty($modSettings['drafts_show_saved_enabled']))
+		echo '
+							<dt>
+								<label for="drafts_show_saved_enabled">', $txt['drafts_show_saved_enabled'], '</label>
+							</dt>
+							<dd>
+								<input type="hidden" name="default_options[drafts_show_saved_enabled]" value="0" />
+								<label for="drafts_show_saved_enabled"><input type="checkbox" name="default_options[drafts_show_saved_enabled]" id="drafts_show_saved_enabled" value="1"', !empty($context['member']['options']['drafts_show_saved_enabled']) ? ' checked="checked"' : '', ' class="input_check" /></label>
+							</dd>';
 
 	echo '
 							<dt>

BIN
Themes/default/images/admin/feature_dr.png


+ 7 - 0
Themes/default/languages/Admin.english.php

@@ -492,6 +492,13 @@ $txt['manageposts_topic_settings'] = 'Topic Settings';
 $txt['manageposts_topic_settings_description'] = 'Here you can set all settings involving topics.';
 $txt['manageposts_topic_settings_submit'] = 'Save';
 
+$txt['managedrafts_settings'] = 'Draft Settings';
+$txt['managedrafts_settings_description'] = 'Here you can set all settings involving drafts.';
+$txt['managedrafts_submit'] = 'Save';
+$txt['manage_drafts'] = 'Drafts';
+$txt['drafts_autosave'] = 'Enable the autosaving of posts as drafts';
+$txt['drafts_autosave_frequency'] = 'How often should drafts be autosaved?';
+
 $txt['removeNestedQuotes'] = 'Remove nested quotes when quoting';
 $txt['enableEmbeddedFlash'] = 'Embed flash into posts';
 $txt['enableEmbeddedFlash_warning'] = 'may be a security risk!';

+ 41 - 0
Themes/default/languages/Drafts.english.php

@@ -0,0 +1,41 @@
+<?php
+// Version: 2.1; Profile
+
+global $scripturl, $context;
+
+// profile
+$txt['drafts_show'] = 'Show Drafts';
+$txt['drafts_autosave_enabled'] = 'Enable the automatic saving of drafts.';
+$txt['drafts_show_saved_enabled'] = 'Enable selection of drafts from posting screens.';
+
+// misc
+$txt['drafts'] = 'Drafts';
+$txt['draft_save'] = 'Save Draft';
+$txt['draft_save_note'] = 'This will save the text of your post, but it will not save attachments, poll or event information.';
+$txt['draft_none'] = 'You have no drafts.';
+$txt['draft_edit'] = 'Edit draft';
+$txt['draft_load'] = 'Load drafts';
+$txt['draft_hide'] = 'Hide drafts';
+$txt['draft_delete'] = 'Delete draft';
+$txt['draft_saved_on'] = 'Draft last saved';
+$txt['draft_days_ago'] = '%s days ago';
+$txt['draft_retain'] = 'this will be retained for %s more days';
+$txt['draft_remove'] = 'Remove this draft';
+$txt['draft_saved'] = 'The contents have been saved as a draft, and will be accessible from the <a href="%1$s">Show Drafts area</a> of your profile.';
+$txt['draft_pm_saved'] = 'The contents have been saved as a draft, and will be accessible from the <a href="%1$s">Show Drafts area</a> of your message center.';
+
+// Admin options
+$txt['drafts_autosave_enabled'] = 'Enable automatic saving of drafts';
+$txt['drafts_autosave_enabled_subnote'] = 'This will automatically save user drafts in the background on a given frequency.  The user must also have the proper permissions';
+$txt['drafts_show_saved_enabled'] = 'Enable the selection of drafts from the posting screen';
+$txt['drafts_show_saved_enabled_subnote'] = 'This will allow the user to select and load appropriate drafts from the posting screen. The user must also have the proper permissions';
+$txt['drafts_keep_days'] = 'Maximum number of days to keep a draft';
+$txt['drafts_keep_days_subnote'] = 'Enter 0 to keep drafts indefinitely';
+$txt['drafts_autosave_frequency'] = 'How often should drafts be autosaved?';
+$txt['drafts_autosave_frequency_subnote'] = 'The minimum allowable value is 30 seconds';
+$txt['drafts_pm_enabled'] = 'Enable the saving of PM drafts';
+$txt['drafts_post_enabled'] = 'Enable the saving of Post drafts';
+$txt['drafts_none'] = 'No Subject';
+$txt['drafts_saved'] = 'Draft was sucessfuly saved';
+
+?>

+ 2 - 0
Themes/default/languages/Errors.english.php

@@ -37,6 +37,7 @@ $txt['feature_disabled'] = 'Sorry, this feature is disabled.';
 $txt['feature_no_exists'] = 'Sorry, this feature doesn\'t exist.';
 $txt['couldnt_connect'] = 'Could not connect to server or could not find file';
 $txt['no_board'] = 'The board you specified doesn\'t exist';
+$txt['no_message'] = 'The message is no longer available';
 $txt['cant_split'] = 'You are not allowed to split topics';
 $txt['cant_merge'] = 'You are not allowed to merge topics';
 $txt['no_topic_id'] = 'You specified an invalid topic ID.';
@@ -252,6 +253,7 @@ $txt['error_wrong_verification_answer'] = 'You did not answer the verification q
 $txt['error_need_verification_code'] = 'Please enter the verification code below to continue to the results.';
 $txt['error_bad_file'] = 'Sorry but the file specified could not be opened: %1$s';
 $txt['error_bad_line'] = 'The line you specified is invalid.';
+$txt['error_draft_not_saved'] = 'There was an error saving the draft';
 
 $txt['smiley_not_found'] = 'Smiley not found.';
 $txt['smiley_has_no_code'] = 'No code for this smiley was given.';

+ 3 - 0
Themes/default/languages/ManageMaintenance.english.php

@@ -157,6 +157,9 @@ $txt['maintain_old_choose'] = 'Specific Boards (click to select all)';
 $txt['maintain_old_remove'] = 'Remove now';
 $txt['maintain_old_confirm'] = 'Are you really sure you want to delete old posts now?\\n\\nThis cannot be undone!';
 
+$txt['maintain_old_drafts'] = 'Remove Old Drafts';
+$txt['maintain_old_drafts_days'] = 'Remove all drafts older than';
+$txt['maintain_old_drafts_confirm'] = 'Are you really sure you want to delete old drafts now?\\n\\nThis cannot be undone!';
 $txt['maintain_members'] = 'Remove Inactive Members';
 $txt['maintain_members_ungrouped'] = 'Ungrouped Members <span class="smalltext">(Members with no assigned groups)</span>';
 $txt['maintain_members_since1'] = 'Remove all members who have not';

+ 17 - 0
Themes/default/languages/ManagePermissions.english.php

@@ -237,6 +237,23 @@ $txt['permissionhelp_poll_remove'] = 'This permission allows removal of polls.';
 $txt['permissionname_poll_remove_own'] = 'Own poll';
 $txt['permissionname_poll_remove_any'] = 'Any poll';
 
+$txt['permissionname_post_draft'] = 'Save drafts of new posts';
+$txt['permissionname_simple_post_draft'] = 'Save drafts of new posts';
+$txt['permissionhelp_post_draft'] = 'This permission allows users to save drafts of thier posts so they can complete them later.';
+$txt['permissionhelp_simple_post_draft'] = 'This permission allows users to save drafts of thier posts so they can complete them later.';
+$txt['permissionname_post_autosave_draft'] = 'Automaticaly save drafts of new posts';
+$txt['permissionname_simple_post_autosave_draft'] = 'Automaticaly save drafts of new posts';
+$txt['permissionhelp_post_autosave_draft'] = 'This permission allows users to have their posts autosaved as drafts so they can avoid loosing their work in the event of a timeout, disconnection or other error.  The autosave schedule is defined in the admin panel';
+$txt['permissionhelp_simple_post_autosave_draft'] = 'This permission allows users to have their posts autosaved as drafts so they can avoid loosing their work in the event of a timeout, disconnection or other error.  The autosave schedule is defined in the admin panel';
+$txt['permissionname_pm_autosave_draft'] = 'Automaticaly save drafts of new PM\'s';
+$txt['permissionname_simple_pm_autosave_draft'] = 'Automaticaly save drafts of new PM\'s';
+$txt['permissionhelp_pm_autosave_draft'] = 'This permission allows users to have their posts autosaved as drafts so they can avoid loosing their work in the event of a timeout, disconnection or other error.  The autosave schedule is defined in the admin panel';
+$txt['permissionhelp_simple_post_autosave_draft'] = 'This permission allows users to have their posts autosaved as drafts so they can avoid loosing their work in the event of a timeout, disconnection or other error.  The autosave schedule is defined in the admin panel';
+$txt['permissionname_pm_draft'] = 'Save drafts of personal messages';
+$txt['permissionname_simple_pm_draft'] = 'Save drafts of personal messages';
+$txt['permissionhelp_pm_draft'] = 'This permission allows users to save drafts of thier personal messages so they can complete them later.';
+$txt['permissionhelp_simple_pm_draft'] = 'This permission allows users to save drafts of thier personal messages so they can complete them later.';
+
 $txt['permissiongroup_approval'] = 'Post Moderation';
 $txt['permissionname_approve_posts'] = 'Approve items awaiting moderation';
 $txt['permissionhelp_approve_posts'] = 'This permission allows a user to approve all unapproved items on a board.';

+ 3 - 0
Themes/default/languages/ManageScheduledTasks.english.php

@@ -57,4 +57,7 @@ $txt['scheduled_log_time_taken_seconds'] = '%1$d seconds';
 $txt['scheduled_log_empty_log'] = 'Clear Log';
 $txt['scheduled_log_empty_log_confirm'] = 'Are you sure you want to completely clear the log?';
 
+$txt['scheduled_task_remove_old_drafts'] = 'Remove old drafts';
+$txt['scheduled_task_desc_remove_old_drafts'] = 'Deletes drafts older than the number of days defined in the draft settings in the admin panel.';
+
 ?>

+ 2 - 0
Themes/default/languages/ManageSettings.english.php

@@ -300,6 +300,8 @@ $txt['core_settings_welcome_msg'] = 'Welcome to Your New Forum';
 $txt['core_settings_welcome_msg_desc'] = 'To get you started we suggest you select which of SMF\'s core features you want to enable. We\'d recommend only enabling with those features you need!';
 $txt['core_settings_item_cd'] = 'Calendar';
 $txt['core_settings_item_cd_desc'] = 'Enabling this feature will open up a selection of options to enable your users to view the calendar, add and review events, see users birthdates on a calendar and much, much more.';
+$txt['core_settings_item_dr'] = 'Drafts';
+$txt['core_settings_item_dr_desc'] = 'Enabling this feature will allow users to save drafts of their posts so they can return to them later to post them.';
 $txt['core_settings_item_cp'] = 'Advanced Profile Fields';
 $txt['core_settings_item_cp_desc'] = 'This enables you to hide standard profile fields, add profile fields to registration, and create new profile fields for your forum.';
 $txt['core_settings_item_k'] = 'Karma';

+ 12 - 10
Themes/default/languages/Post.english.php

@@ -83,8 +83,10 @@ $txt['notifyUnsubscribe'] = 'Unsubscribe to this topic by clicking here';
 $txt['lock_after_post'] = 'Lock after Post';
 $txt['notify_replies'] = 'Notify me of replies.';
 $txt['lock_topic'] = 'Lock this topic.';
-$txt['shortcuts'] = 'shortcuts: hit alt+s to submit/post or alt+p to preview';
-$txt['shortcuts_firefox'] = 'shortcuts: hit shift+alt+s to submit/post or shift+alt+p to preview';
+$txt['shortcuts'] = 'shortcuts: alt+s submit/post or alt+p preview';
+$txt['shortcuts_drafts'] = 'shortcuts: alt+s submit/post, alt+p preview or alt+d save draft';
+$txt['shortcuts_firefox'] = 'shortcuts: shift+alt+s submit/post or shift+alt+p preview';
+$txt['shortcuts_drafts_firefox'] = 'shortcuts: shift+alt+s submit/post, shift+alt+p preview or shift+alt+d save draft';
 $txt['option'] = 'Option';
 $txt['reset_votes'] = 'Reset Vote Count';
 $txt['reset_votes_check'] = 'Check this if you want to reset all vote counts to 0.';
@@ -193,32 +195,32 @@ $txt['digest_mod_act_move'] = '"%1$s" was moved';
 $txt['digest_mod_act_merge'] = '"%1$s" was merged';
 $txt['digest_mod_act_split'] = '"%1$s" was split';
 
-$txt['attach_error_title'] = 'Error uploading your attachments.';
+$txt['attach_error_title'] = 'Error uploading attachments.';
 $txt['attach_warning'] = 'There was a problem during the uploading of <strong>%1$s</strong>.';
 $txt['attach_check_nag'] = 'Unable to continue due to incomplete data (%1$s).';
 $txt['attach_continue'] = 'Continue';
 $txt['attach_max_total_file_size'] = 'Sorry, you are out of attachment space. The total attachment size allowed per post is %1$s KB. Space remaining is %2$s kB.';
 $txt['attach_folder_warning'] = 'The attachments directory can not be located. Please notify an administrator of this problem.';
-$txt['attach_folder_admin_warning'] = 'The path to the attachments directory (%1$s) is incorrect. Please correct it in the attachment settings.';
-$txt['attach_limit_nag'] = 'Sorry, but you have reached the maximum number of attachments allowed per post.';
+$txt['attach_folder_admin_warning'] = 'The path to the attachments directory (%1$s) is incorrect. Please correct it in the attachment settings area of your admin panel.';
+$txt['attach_limit_nag'] = 'You have reached the maximum number of attachments allowed per post.';
 $txt['attach_no_upload'] = 'There was a problem and your attachments could not be uploaded';
 $txt['attach_remaining'] = '%1$d remaining';
 $txt['attach_available'] = '%1$s KB available';
 $txt['attach_kb'] = ' (%1$s KB)';
-$txt['attach_0_byte_file'] = 'This file appears to be empty. Please contact your forum administrator if this continues to be a problem';
+$txt['attach_0_byte_file'] = 'The file appears to be empty. Please contact your forum administrator if this continues to be a problem';
 $txt['attached_files_in_session'] = '<em>The above underlined file(s) have been uploaded but will not be attached to this post until it is submitted.</em>';
 
-$txt['attach_php_error'] = 'Due to an error, your attachment could not be uploaded. Please contact the forum administrator if this continues to be a problem.';
-$txt['php_upload_error_1'] = 'The uploaded file exceeds the upload_max_filesize directive in php.ini. Please contact your host if unable to rectify.';
+$txt['attach_php_error'] = 'Due to an error, your attachment could not be uploaded. Please contact the forum administrator if this problem continues.';
+$txt['php_upload_error_1'] = 'The uploaded file exceeds the upload_max_filesize directive in php.ini. Please contact your host if you are unable to correct this issue.';
 $txt['php_upload_error_3'] = 'The uploaded file was only partially uploaded. This is a PHP related error. Please contact your host if this problem continues.';
 $txt['php_upload_error_4'] = 'No file was uploaded. This is a PHP related error. Please contact your host if this problem continues.';
-$txt['php_upload_error_6'] = 'Unable to save. Missing a temporary folder. Please contact your host if unable to rectify.';
+$txt['php_upload_error_6'] = 'Unable to save. Missing a temporary folder. Please contact your host if you are unable to correct this problem.';
 $txt['php_upload_error_7'] = 'Failed to write file to disk. This is a PHP related error. Please contact your host if this problem continues.';
 $txt['php_upload_error_8'] = 'A PHP extension stopped the file upload. This is a PHP related error. Please contact your host if this problem continues.';
 $txt['error_temp_attachments_new'] = 'There are attachments which you had previously attached but not posted. These attachments are still attached to this post. This post does need to be submitted before these attachments are either saved or removed. You can do that <a href="#postAttachment">here</a>';
 $txt['error_temp_attachments_found'] = 'The following attachments were found which you had previously attached to another post but not posted. It is advisable that you do not post until these are either removed or that post has been submited.<br />Click %1$s to remove those attachments. Or %2$s to return to that post.%3$s';
 $txt['error_temp_attachments_lost'] = 'The following attachments were found which you had previously attached to another post but not posted. It is advisable that you do not upload any more attachments until these are removed or that post has been submitedd.<br />Click %1$s to remove these attachments.%2$s';
-$txt['error_temp_attachments_gone'] = 'Those attachments are now removed and you have been returned to the page you were previously on';
+$txt['error_temp_attachments_gone'] = 'Those attachments have now been removed and you have been returned to the page you were previously on';
 $txt['error_temp_attachments_flushed'] = 'Please note that any files which had been previously attached but not posted. Have now been removed.';
 $txt['error_topic_already_announced'] = 'Please note that this topic has already been announced.';
 

+ 2 - 0
Themes/default/languages/Reports.english.php

@@ -21,6 +21,8 @@ $txt['results'] = 'Results';
 
 // Board permissions
 $txt['board_perms_permission'] = 'Permission';
+$txt['board_perms_allow'] = 'Allow';
+$txt['board_perms_deny'] = 'Deny';
 $txt['board_perms_name_announce_topic'] = 'Announce topic';
 $txt['board_perms_name_approve_posts'] = 'Approve posts';
 $txt['board_perms_name_delete_any'] = 'Delete any posts';

+ 7 - 5
Themes/default/languages/index.english.php

@@ -149,7 +149,7 @@ $txt['remove_message'] = 'Remove this message';
 $txt['online_users'] = 'Users Online';
 $txt['personal_message'] = 'Personal Message';
 $txt['jump_to'] = 'Jump to';
-$txt['go'] = 'Go!';
+$txt['go'] = 'Go';
 $txt['are_sure_remove_topic'] = 'Are you sure you want to remove this topic?';
 $txt['yes'] = 'Yes';
 $txt['no'] = 'No';
@@ -382,8 +382,10 @@ $txt['new_poll'] = 'New poll';
 $txt['poll_question'] = 'Question';
 $txt['poll_vote'] = 'Submit Vote';
 $txt['poll_total_voters'] = 'Total Members Voted';
-$txt['shortcuts'] = 'shortcuts: hit alt+s to submit/post or alt+p to preview';
-$txt['shortcuts_firefox'] = 'shortcuts: hit shift+alt+s to submit/post or shift+alt+p to preview';
+$txt['shortcuts'] = 'shortcuts: alt+s submit/post, alt+p preview';
+$txt['shortcuts_firefox'] = 'shortcuts: shift+alt+s submit/post, shift+alt+p preview';
+$txt['shortcuts_drafts'] = 'or alt+d save draft';
+$txt['shortcuts_drafts_firefox'] = 'or shift+alt+d save draft';
 $txt['poll_results'] = 'View results';
 $txt['poll_lock'] = 'Lock Voting';
 $txt['poll_unlock'] = 'Unlock Voting';
@@ -453,7 +455,7 @@ $txt['security_risk'] = 'MAJOR SECURITY RISK:';
 $txt['not_removed'] = 'You have not removed ';
 $txt['not_removed_extra'] = '%1$s is a backup of %2$s that was not generated by SMF. It can be accessed directly and used to gain unauthorised access to your forum. You should delete it immediately.';
 $txt['generic_warning'] = 'Warning';
-$txt['agreement_missing'] = 'Even though you are requiring to accept the registration agreement, the file (agreement.txt) doesn\'t exists!';
+$txt['agreement_missing'] = 'You are requiring new users to accept a registration agreement, however the file (agreement.txt) doesn\'t exist!';
 
 $txt['cache_writable'] = 'The cache directory is not writable - this will adversely affect the performance of your forum.';
 
@@ -702,7 +704,7 @@ $txt['preview_title'] = 'Preview post';
 $txt['preview_fetch'] = 'Fetching preview...';
 $txt['preview_new'] = 'New message';
 $txt['pm_error_while_submitting'] = 'The following error or errors occurred while sending this personal message:';
-$txt['error_while_submitting'] = 'The following error or errors occurred while posting this message:';
+$txt['error_while_submitting'] = 'The message has the following error or errors that must be corrected before continuing:';
 $txt['error_old_topic'] = 'Warning: this topic has not been posted in for at least %1$d days.<br />Unless you\'re sure you want to reply, please consider starting a new topic.';
 
 $txt['split_selected_posts'] = 'Selected posts';

+ 18 - 3
Themes/default/scripts/admin.js

@@ -134,7 +134,10 @@ smf_ViewVersions.prototype.swapOption = function (oSendingElement, sName)
 {
 	// If it is undefined, or currently off, turn it on - otherwise off.
 	this.oSwaps[sName] = !(sName in this.oSwaps) || !this.oSwaps[sName];
-	document.getElementById(sName).style.display = this.oSwaps[sName] ? '' : 'none';
+	if (this.oSwaps[sName])
+		$("#" + sName).show(300);
+	else
+		$("#" + sName).hide(300);
 
 	// Unselect the link and return false.
 	oSendingElement.blur();
@@ -477,12 +480,24 @@ function toggleBreakdown(id_group, forcedisplayType)
 	if (typeof(forcedisplayType) != "undefined")
 		displayType = forcedisplayType;
 
+	// swap the image
+	document.getElementById("group_toggle_img_" + id_group).src = smf_images_url + "/" + (displayType == "none" ? "selected" : "selected_open") + ".png";
+
+	// show or hide the elements
+	var aContainer = new Array();
 	for (i = 0; i < groupPermissions[id_group].length; i++)
 	{
-		document.getElementById("perm_div_" + id_group + "_" + groupPermissions[id_group][i]).style.display = displayType
+		var oContainerTemp = document.getElementById("perm_div_" + id_group + "_" + groupPermissions[id_group][i]);
+		if (typeof(oContainerTemp) == 'object' && oContainerTemp != null)
+			aContainer[i] = oContainerTemp;
 	}
+	if (displayType == "none")
+		$(aContainer).fadeOut();
+	else
+		$(aContainer).show();
+		
+	// remove or add the separators
 	document.getElementById("group_hr_div_" + id_group).style.display = displayType
-	document.getElementById("group_toggle_img_" + id_group).src = smf_images_url + "/" + (displayType == "none" ? "selected" : "selected_open") + ".png";
 
 	return false;
 }

+ 186 - 0
Themes/default/scripts/drafts.js

@@ -0,0 +1,186 @@
+// The draft save object
+function smf_DraftAutoSave(oOptions)
+{
+	this.opt = oOptions;
+	this.bInDraftMode = false;
+	this.sCurDraftId = '';
+	this.oCurDraftDiv = null;
+	this.interval_id = null;
+	this.oDraftHandle = window;
+	this.sLastSaved = '';
+	this.bPM = this.opt.bPM ? true : false;
+	addLoadEvent(this.opt.sSelf + '.init();');
+}
+
+// Start our self calling routine
+smf_DraftAutoSave.prototype.init = function ()
+{
+	if (this.opt.iFreq > 0)
+	{
+		// start the autosave timer
+		this.interval_id = setInterval(this.opt.sSelf + '.draft' + (this.bPM ? 'PM' : '') + 'Save();', this.opt.iFreq);
+
+		// Set up window focus and blur events
+		this.oDraftHandle.instanceRef = this;
+		this.oDraftHandle.onblur = function (oEvent) {return this.instanceRef.draftBlur(oEvent);};
+		this.oDraftHandle.onfocus = function (oEvent) {return this.instanceRef.draftFocus(oEvent);};
+	}
+}
+
+// Moved away from the page, where did you go? ... till you return we pause autosaving
+smf_DraftAutoSave.prototype.draftBlur = function(oEvent)
+{
+	// save what we have and turn of the autosave
+    if (this.bPM)
+        this.draftPMSave();
+    else
+        this.draftSave();
+	clearInterval(this.interval_id);
+	this.interval_id = 0;
+}
+
+// Since your back we resume the autosave timer
+smf_DraftAutoSave.prototype.draftFocus = function(oEvent)
+{
+	this.interval_id = setInterval(this.opt.sSelf + '.draft' + (this.bPM ? 'PM' : '') + 'Save();', this.opt.iFreq);
+}
+
+// Make the call to save this draft in the background
+smf_DraftAutoSave.prototype.draftSave = function ()
+{
+	// nothing to save or already posting?
+	if (isEmptyText($('#' + this.opt.sSceditorID).data("sceditor").getText()) || smf_formSubmitted)
+		return false;
+
+	// Still saving the last one or other?
+	if (this.bInDraftMode)
+		this.draftCancel();
+
+	// Flag that we are saving a draft
+	document.getElementById('throbber').style.display = '';
+	this.bInDraftMode = true;
+
+	// Get the form elements that we want to save
+	var aSections = [
+		'topic=' + parseInt(document.forms.postmodify.elements['topic'].value),
+		'id_draft=' + parseInt(document.forms.postmodify.elements['id_draft'].value),
+		'subject=' + escape(document.forms.postmodify['subject'].value.replace(/&#/g, "&#38;#").php_to8bit()).replace(/\+/g, "%2B"),
+		'message=' + escape($('#' + this.opt.sSceditorID).data("sceditor").getText().replace(/&#/g, "&#38;#").php_to8bit()).replace(/\+/g, "%2B"),
+		'icon=' + escape(document.forms.postmodify['icon'].value.replace(/&#/g, "&#38;#").php_to8bit()).replace(/\+/g, "%2B"),
+		'save_draft=true',
+		smf_session_var + '=' + smf_session_id,
+	];
+
+	// Get the locked an/or sticky values if they have been selected or set that is
+	if (this.opt.sType == 'post')
+	{
+		if (document.getElementById('check_lock').checked)
+			aSections[aSections.length] = 'lock=1';
+		if (document.getElementById('check_sticky').checked)
+			aSections[aSections.length] = 'sticky=1';
+	}
+
+	// keep track of source or wysiwyg
+	aSections[aSections.length] = 'message_mode=' + $("#message").data("sceditor").inSourceMode();
+
+	// Send in document for saving and hope for the best
+	sendXMLDocument.call(this, smf_prepareScriptUrl(smf_scripturl) + "action=post2;board=" + this.opt.iBoard + ";xml", aSections.join("&"), this.onDraftDone);
+}
+
+// Make the call to save this PM draft in the background
+smf_DraftAutoSave.prototype.draftPMSave = function ()
+{
+	// nothing to save?
+	if (isEmptyText(document.forms.postmodify['message']))
+		return false;
+
+	// Still saving the last one or some other?
+	if (this.bInDraftMode)
+		this.draftCancel();
+
+	// Flag that we are saving
+	document.getElementById('throbber').style.display = '';
+	this.bInDraftMode = true;
+
+	// Get the to and bcc values
+	var aTo = this.draftGetRecipient('recipient_to[]');
+	var aBcc = this.draftGetRecipient('recipient_bcc[]');
+
+	// Get the rest of the form elements that we want to save, and load them up
+	var aSections = [
+		'replied_to=' + parseInt(document.forms.postmodify.elements['replied_to'].value),
+		'id_pm_draft=' + parseInt(document.forms.postmodify.elements['id_pm_draft'].value),
+		'subject=' + escape(document.forms.postmodify['subject'].value.replace(/&#/g, "&#38;#").php_to8bit()).replace(/\+/g, "%2B"),
+		'message=' + escape(document.forms.postmodify['message'].value.replace(/&#/g, "&#38;#").php_to8bit()).replace(/\+/g, "%2B"),
+		'recipient_to=' + aTo,
+		'recipient_bcc=' + aBcc,
+		'save_draft=true',
+		smf_session_var + '=' + smf_session_id,
+	];
+
+	// Saving a copy in the outbox?
+	if (document.getElementById('outbox'))
+		aSections[aSections.length] = 'outbox=' + parseInt(document.getElementById('outbox').value);
+
+	// account for wysiwyg
+	if (this.opt.sType == 'post')
+		aSections[aSections.length] = 'message_mode=' + parseInt(document.forms.postmodify.elements['message_mode'].value);
+
+	// Send in (post) the document for saving
+	sendXMLDocument.call(this, smf_prepareScriptUrl(smf_scripturl) + "action=pm;sa=send2;xml", aSections.join("&"), this.onDraftDone);
+}
+
+// Callback function of the XMLhttp request for saving the draft message
+smf_DraftAutoSave.prototype.onDraftDone = function (XMLDoc)
+{
+	// If it is not valid then clean up
+	if (!XMLDoc || !XMLDoc.getElementsByTagName('draft'))
+		return this.draftCancel();
+
+	// Grab the returned draft id and saved time from the response
+	this.sCurDraftId = XMLDoc.getElementsByTagName('draft')[0].getAttribute('id');
+	this.sLastSaved = XMLDoc.getElementsByTagName('draft')[0].childNodes[0].nodeValue;
+
+	// Update the form to show we finished, if the id is not set, then set it
+	document.getElementById(this.opt.sLastID).value = this.sCurDraftId;
+	oCurDraftDiv = document.getElementById(this.opt.sLastNote);
+	setInnerHTML(oCurDraftDiv, this.sLastSaved);
+
+	// hide the saved draft infobox in the event they pressed the save draft button at some point
+	if (this.opt.sType == 'post')
+		document.getElementById('draft_section').style.display = 'none';
+
+	// thank you sir, may I have another
+	this.bInDraftMode = false;
+	document.getElementById('throbber').style.display = 'none';
+}
+
+// function to retrieve the to and bcc values from the pseudo arrays
+smf_DraftAutoSave.prototype.draftGetRecipient = function (sField)
+{
+	var oRecipient = document.forms.postmodify.elements[sField];
+	var aRecipient = []
+
+	if (typeof(oRecipient) != 'undefined')
+	{
+		// just one recipient
+		if ('value' in oRecipient)
+			aRecipient.push(parseInt(oRecipient.value));
+		else
+		{
+			// or many !
+			for (var i = 0, n = oRecipient.length; i < n; i++)
+				aRecipient.push(parseInt(oRecipient[i].value));
+		}
+	}
+	return aRecipient;
+}
+
+// If another auto save came in with one still pending
+smf_DraftAutoSave.prototype.draftCancel = function ()
+{
+	// can we do anything at all ... do we want to (e.g. sequence our async events?)
+	// @todo if not remove this function
+	this.bInDraftMode = false;
+	document.getElementById('throbber').style.display = 'none';
+}

+ 20 - 76
Themes/default/scripts/script.js

@@ -6,16 +6,6 @@ var smf_editorArray = new Array();
 var ua = navigator.userAgent.toLowerCase();
 
 var is_opera = ua.indexOf('opera') != -1;
-var is_opera5 = ua.indexOf('opera/5') != -1 || ua.indexOf('opera 5') != -1;
-var is_opera6 = ua.indexOf('opera/6') != -1 || ua.indexOf('opera 6') != -1;
-var is_opera7 = ua.indexOf('opera/7') != -1 || ua.indexOf('opera 7') != -1;
-var is_opera8 = ua.indexOf('opera/8') != -1 || ua.indexOf('opera 8') != -1;
-var is_opera9 = ua.indexOf('opera/9') != -1 || ua.indexOf('opera 9') != -1;
-var is_opera95 = ua.indexOf('opera/9.5') != -1 || ua.indexOf('opera 9.5') != -1;
-var is_opera96 = ua.indexOf('opera/9.6') != -1 || ua.indexOf('opera 9.6') != -1;
-var is_opera10 = (ua.indexOf('opera/9.8') != -1 || ua.indexOf('opera 9.8') != -1 || ua.indexOf('opera/10.') != -1 || ua.indexOf('opera 10.') != -1) || ua.indexOf('version/10.') != -1;
-var is_opera95up = is_opera95 || is_opera96 || is_opera10;
-
 var is_ff = (ua.indexOf('firefox') != -1 || ua.indexOf('iceweasel') != -1 || ua.indexOf('icecat') != -1 || ua.indexOf('shiretoko') != -1 || ua.indexOf('minefield') != -1) && !is_opera;
 var is_gecko = ua.indexOf('gecko') != -1 && !is_opera;
 
@@ -24,44 +14,17 @@ var is_safari = ua.indexOf('applewebkit') != -1 && !is_chrome;
 var is_webkit = ua.indexOf('applewebkit') != -1;
 
 var is_ie = ua.indexOf('msie') != -1 && !is_opera;
-var is_ie4 = is_ie && ua.indexOf('msie 4') != -1;
-var is_ie5 = is_ie && ua.indexOf('msie 5') != -1;
-var is_ie50 = is_ie && ua.indexOf('msie 5.0') != -1;
-var is_ie55 = is_ie && ua.indexOf('msie 5.5') != -1;
-var is_ie5up = is_ie && !is_ie4;
-var is_ie6 = is_ie && ua.indexOf('msie 6') != -1;
-var is_ie6up = is_ie5up && !is_ie55 && !is_ie5;
-var is_ie6down = is_ie6 || is_ie5 || is_ie4;
-var is_ie7 = is_ie && ua.indexOf('msie 7') != -1;
-var is_ie7up = is_ie6up && !is_ie6;
-var is_ie7down = is_ie7 || is_ie6 || is_ie5 || is_ie4;
-
-var is_ie8 = is_ie && ua.indexOf('msie 8') != -1;
-var is_ie8up = is_ie8 && !is_ie7down;
-
 var is_iphone = ua.indexOf('iphone') != -1 || ua.indexOf('ipod') != -1;
 var is_android = ua.indexOf('android') != -1;
 
 var ajax_indicator_ele = null;
 
-// Define document.getElementById for Internet Explorer 4.
-if (!('getElementById' in document) && 'all' in document)
-	document.getElementById = function (sId) {
-		return document.all[sId];
-	}
-
-// Define XMLHttpRequest for IE 5 and above. (don't bother for IE 4 :/.... works in Opera 7.6 and Safari 1.2!)
-else if (!('XMLHttpRequest' in window) && 'ActiveXObject' in window)
+// Define XMLHttpRequest for IE
+if (!('XMLHttpRequest' in window) && 'ActiveXObject' in window)
 	window.XMLHttpRequest = function () {
-		return new ActiveXObject(is_ie5 ? 'Microsoft.XMLHTTP' : 'MSXML2.XMLHTTP');
+		return new ActiveXObject('MSXML2.XMLHTTP');
 	};
 
-// Ensure the getElementsByTagName exists.
-if (!'getElementsByTagName' in document && 'all' in document)
-	document.getElementsByTagName = function (sName) {
-		return document.all.tags[sName];
-	}
-
 // Some older versions of Mozilla don't have this, for some reason.
 if (!('forms' in document))
 	document.forms = document.getElementsByTagName('form');
@@ -340,7 +303,7 @@ function reqOverlayDiv(desktopURL, sHeader, sIcon)
 {
 	// Set up our div details
 	var sAjax_indicator = '<div class="centertext"><img src="' + smf_images_url + '/loading.gif" ></div>';
-	var sIcon = smf_images_url + '/' + typeof(sIcon) == 'string' ? sIcon : 'helptopics.png';
+	var sIcon = smf_images_url + '/' + (typeof(sIcon) == 'string' ? sIcon : 'helptopics.png');
 	var sHeader = typeof(sHeader) == 'string' ? sHeader : help_popup_heading_text;
 	
 	// Create the div that we are going to load
@@ -503,7 +466,10 @@ function surroundText(text1, text2, oTextHandle)
 function isEmptyText(theField)
 {
 	// Copy the value so changes can be made..
-	var theValue = theField.value;
+	if (typeof(theField) == 'string')
+		var theValue = theField;
+	else
+		var theValue = theField.value;
 
 	// Strip whitespace off the left side.
 	while (theValue.length > 0 && (theValue.charAt(0) == ' ' || theValue.charAt(0) == '\t'))
@@ -529,10 +495,6 @@ function submitonce(theform)
 }
 function submitThisOnce(oControl)
 {
-	// Hateful, hateful fix for Safari 1.3 beta.
-	if (is_safari)
-		return !smf_formSubmitted;
-
 	// oControl might also be a form.
 	var oForm = 'form' in oControl ? oControl.form : oControl;
 
@@ -764,7 +726,7 @@ function expandPages(spanNode, baseURL, firstPage, lastPage, perPage)
 		replacement += '<a class="navPages" href="' + baseURL.replace(/%1\$d/, i).replace(/%%/g, '%') + '">' + (1 + i / perPage) + '</a> ';
 
 	if (oldLastPage > 0)
-		replacement += '<span style="font-weight: bold; cursor: ' + (is_ie && !is_ie6up ? 'hand' : 'pointer') + ';" onclick="expandPages(this, \'' + baseURL + '\', ' + lastPage + ', ' + oldLastPage + ', ' + perPage + ');"> ... </span> ';
+		replacement += '<span style="font-weight: bold; cursor: pointer" onclick="expandPages(this, \'' + baseURL + '\', ' + lastPage + ', ' + oldLastPage + ', ' + perPage + ');"> ... </span> ';
 
 	// Replace the dots by the new page links.
 	setInnerHTML(spanNode, replacement);
@@ -953,7 +915,12 @@ smc_Toggle.prototype.changeState = function(bCollapse, bInit)
 
 		var oContainer = document.getElementById(this.opt.aSwappableContainers[i]);
 		if (typeof(oContainer) == 'object' && oContainer != null)
-			oContainer.style.display = bCollapse ? 'none' : '';
+		{
+			if (bCollapse)
+				$(oContainer).slideUp();
+			else
+				$(oContainer).slideDown();
+		}
 	}
 
 	// Update the new state.
@@ -988,12 +955,6 @@ function ajax_indicator(turn_on)
 
 	if (ajax_indicator_ele != null)
 	{
-		if (navigator.appName == 'Microsoft Internet Explorer' && !is_ie7up)
-		{
-			ajax_indicator_ele.style.position = 'absolute';
-			ajax_indicator_ele.style.top = document.documentElement.scrollTop;
-		}
-
 		ajax_indicator_ele.style.display = turn_on ? 'block' : 'none';
 	}
 }
@@ -1107,7 +1068,6 @@ JumpTo.prototype.showSelect = function ()
 // Fill the jump to box with entries. Method of the JumpTo class.
 JumpTo.prototype.fillSelect = function (aBoardsAndCategories)
 {
-	var bIE5x = !('implementation' in document);
 	var iIndexPointer = 0;
 
 	// Create an option that'll be above and below the category.
@@ -1116,19 +1076,13 @@ JumpTo.prototype.fillSelect = function (aBoardsAndCategories)
 	oDashOption.disabled = 'disabled';
 	oDashOption.value = '';
 
-	// Reset the events and clear the list (IE5.x only).
-	if (bIE5x)
-	{
-		this.dropdownList.onmouseover = null;
-		this.dropdownList.remove(0);
-	}
 	if ('onbeforeactivate' in document)
 		this.dropdownList.onbeforeactivate = null;
 	else
 		this.dropdownList.onfocus = null;
 
 	// Create a document fragment that'll allowing inserting big parts at once.
-	var oListFragment = bIE5x ? this.dropdownList : document.createDocumentFragment();
+	var oListFragment = document.createDocumentFragment();
 
 	// Loop through all items to be added.
 	for (var i = 0, n = aBoardsAndCategories.length; i < n; i++)
@@ -1138,14 +1092,9 @@ JumpTo.prototype.fillSelect = function (aBoardsAndCategories)
 		// If we've reached the currently selected board add all items so far.
 		if (!aBoardsAndCategories[i].isCategory && aBoardsAndCategories[i].id == this.opt.iCurBoardId)
 		{
-			if (bIE5x)
-				iIndexPointer = this.dropdownList.options.length;
-			else
-			{
 				this.dropdownList.insertBefore(oListFragment, this.dropdownList.options[0]);
 				oListFragment = document.createDocumentFragment();
 				continue;
-			}
 		}
 
 		if (aBoardsAndCategories[i].isCategory)
@@ -1166,9 +1115,6 @@ JumpTo.prototype.fillSelect = function (aBoardsAndCategories)
 	// Add the remaining items after the currently selected item.
 	this.dropdownList.appendChild(oListFragment);
 
-	if (bIE5x)
-		this.dropdownList.options[iIndexPointer].selected = true;
-
 	// Internet Explorer needs this to keep the box dropped down.
 	this.dropdownList.style.width = 'auto';
 	this.dropdownList.focus();
@@ -1209,7 +1155,7 @@ IconList.prototype.initIcons = function ()
 {
 	for (var i = document.images.length - 1, iPrefixLength = this.opt.sIconIdPrefix.length; i >= 0; i--)
 		if (document.images[i].id.substr(0, iPrefixLength) == this.opt.sIconIdPrefix)
-			setOuterHTML(document.images[i], '<div title="' + this.opt.sLabelIconList + '" onclick="' + this.opt.sBackReference + '.openPopup(this, ' + document.images[i].id.substr(iPrefixLength) + ')" onmouseover="' + this.opt.sBackReference + '.onBoxHover(this, true)" onmouseout="' + this.opt.sBackReference + '.onBoxHover(this, false)" style="background: ' + this.opt.sBoxBackground + '; cursor: ' + (is_ie && !is_ie6up ? 'hand' : 'pointer') + '; padding: 3px; text-align: center;"><img src="' + document.images[i].src + '" alt="' + document.images[i].alt + '" id="' + document.images[i].id + '" style="margin: 0px; padding: ' + (is_ie ? '3px' : '3px 0px 3px 0px') + ';" /></div>');
+			setOuterHTML(document.images[i], '<div title="' + this.opt.sLabelIconList + '" onclick="' + this.opt.sBackReference + '.openPopup(this, ' + document.images[i].id.substr(iPrefixLength) + ')" onmouseover="' + this.opt.sBackReference + '.onBoxHover(this, true)" onmouseout="' + this.opt.sBackReference + '.onBoxHover(this, false)" style="background: ' + this.opt.sBoxBackground + '; cursor: pointer; padding: 3px; text-align: center;"><img src="' + document.images[i].src + '" alt="' + document.images[i].alt + '" id="' + document.images[i].id + '" style="margin: 0px; padding: ' + (is_ie ? '3px' : '3px 0px 3px 0px') + ';" /></div>');
 }
 
 // Event for the mouse hovering over the original icon.
@@ -1231,7 +1177,7 @@ IconList.prototype.openPopup = function (oDiv, iMessageId)
 		this.oContainerDiv = document.createElement('div');
 		this.oContainerDiv.id = 'iconList';
 		this.oContainerDiv.style.display = 'none';
-		this.oContainerDiv.style.cursor = is_ie && !is_ie6up ? 'hand' : 'pointer';
+		this.oContainerDiv.style.cursor = 'pointer';
 		this.oContainerDiv.style.position = 'absolute';
 		this.oContainerDiv.style.width = oDiv.offsetWidth + 'px';
 		this.oContainerDiv.style.background = this.opt.sContainerBackground;
@@ -1251,8 +1197,6 @@ IconList.prototype.openPopup = function (oDiv, iMessageId)
 
 	// Set the position of the container.
 	var aPos = smf_itemPos(oDiv);
-	if (is_ie50)
-		aPos[1] += 4;
 
 	this.oContainerDiv.style.top = (aPos[1] + oDiv.offsetHeight) + 'px';
 	this.oContainerDiv.style.left = (aPos[0] - 1) + 'px';
@@ -1588,7 +1532,7 @@ function expandCollapseBoards()
 {
 	var current = document.getElementById("searchBoardsExpand").style.display != "none";
 
-	document.getElementById("searchBoardsExpand").style.display = current ? "none" : "";
+	$("#searchBoardsExpand").slideToggle(300);
 	document.getElementById("expandBoardsIcon").src = smf_images_url + (current ? "/expand.png" : "/collapse.png");
 }
 
@@ -1596,7 +1540,7 @@ function expandCollapseLabels()
 {
 	var current = document.getElementById("searchLabelsExpand").style.display != "none";
 
-	document.getElementById("searchLabelsExpand").style.display = current ? "none" : "";
+	$("#searchLabelsExpand").slideToggle();
 	document.getElementById("expandLabelsIcon").src = smf_images_url + (current ? "/expand.png" : "/collapse.png");
 }
 

+ 16 - 10
Themes/default/scripts/smf_jquery_plugins.js

@@ -25,9 +25,11 @@
 			hoverIntent: {sensitivity: 10, interval: 300, timeout: 50},
 			positionTop: 12,
 			positionLeft: 12,
-			tooltipID: 'smf_tooltip',
-			tooltipClass: 'tooltip',
-			tooltipTextID: 'smf_tooltipText'
+			tooltipID: 'smf_tooltip', // ID used on the outer div
+			tooltipTextID: 'smf_tooltipText', // as above but on the inner div holding the text
+			tooltipClass: 'tooltip', // The class applied to the outer div (that displays on hover), use this in your css
+			tooltipSwapClass: 'smf_swaptip', // a class only used internally, change only if you have a conflict
+			tooltipContent: 'html' // display captured title text as html or text
 		};
 		
 		// account for any user options
@@ -36,7 +38,7 @@
 		// move passed selector titles to a hidden span, then remove the selector title to prevent any default browser actions
 		$(this).each(function()
 		{
-			var sTitle = $('<span class="' + oSettings.tooltipClass + '">' + this.title + '</span>').hide();
+			var sTitle = $('<span class="' + oSettings.tooltipSwapClass + '">' + this.title + '</span>').hide();
 			$(this).append(sTitle).attr('title', '');
 		});
 		
@@ -128,8 +130,8 @@
 			// create the on tip action
 			function smf_tooltip_on(event)
 			{
-				// Grab the text from the hidden span element we created on page load
-				if ($(this).children('.' + oSettings.tooltipClass).text())
+				// If we have text in the hidden span element we created on page load
+				if ($(this).children('.' + oSettings.tooltipSwapClass).text())
 				{
 					// create a ID'ed div with our style class that holds the tooltip info, hidden for now
 					$('body').append('<div id="' + oSettings.tooltipID + '" class="' + oSettings.tooltipClass + '"><div id="' + oSettings.tooltipTextID + '" style="display:none;"></div></div>');
@@ -137,12 +139,16 @@
 					// load information in to our newly created div
 					var $tt = $('#' + oSettings.tooltipID);
 					var $ttContent = $('#' + oSettings.tooltipID + ' #' + oSettings.tooltipTextID);
-				
-					// set the text in the div
-					$ttContent.text($(this).children('.' + oSettings.tooltipClass).text());
-					$tt.show();
+					
+					if (oSettings.tooltipContent == 'html')
+						$ttContent.html($(this).children('.' + oSettings.tooltipSwapClass).html());
+					else
+						$ttContent.text($(this).children('.' + oSettings.tooltipSwapClass).text());
+					
+					oSettings.tooltipContent
 					
 					// show then position or it may postion off screen
+					$tt.show();
 					showTooltip();
 					positionTooltip(event);
 				}

+ 1 - 1
Themes/default/scripts/theme.js

@@ -27,7 +27,7 @@ function smf_codeBoxFix()
 }
 
 // Add a fix for code stuff?
-if ((is_ie && !is_ie4) || is_webkit || is_ff)
+if (is_ie || is_webkit || is_ff)
 	addLoadEvent(smf_codeBoxFix);
 
 // Toggles the element height and width styles of an image.

+ 1 - 5
Themes/default/scripts/topic.js

@@ -242,11 +242,7 @@ QuickModify.prototype.modifyMsg = function (iMessageId)
 
 	// Send out the XMLhttp request to get more info
 	ajax_indicator(true);
-
-	// For IE 5.0 support, 'call' is not yet used.
-	this.tmpMethod = getXMLDocument;
-	this.tmpMethod(smf_prepareScriptUrl(this.opt.sScriptUrl) + 'action=quotefast;quote=' + iMessageId + ';modify;xml', this.onMessageReceived);
-	delete this.tmpMethod;
+	sendXMLDocument.call(this, smf_prepareScriptUrl(this.opt.sScriptUrl) + 'action=quotefast;quote=' + iMessageId + ';modify;xml', this.onMessageReceived);
 }
 
 // The callback function used for the XMLhttp request retrieving the message.

+ 51 - 1
other/install_2-1_mysql.sql

@@ -149,6 +149,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 1, 'poll_vote'),
 	(0, 1, 'post_attachment'),
 	(0, 1, 'post_new'),
+	(0, 1, 'post_draft'),
+	(0, 1, 'post_autosave_draft'),
 	(0, 1, 'post_reply_any'),
 	(0, 1, 'post_reply_own'),
 	(0, 1, 'post_unapproved_topics'),
@@ -161,6 +163,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 1, 'view_attachments'),
 	(2, 1, 'moderate_board'),
 	(2, 1, 'post_new'),
+	(2, 1, 'post_draft'),
+	(2, 1, 'post_autosave_draft'),
 	(2, 1, 'post_reply_own'),
 	(2, 1, 'post_reply_any'),
 	(2, 1, 'post_unapproved_topics'),
@@ -194,6 +198,8 @@ VALUES (-1, 1, 'poll_view'),
 	(2, 1, 'view_attachments'),
 	(3, 1, 'moderate_board'),
 	(3, 1, 'post_new'),
+	(3, 1, 'post_draft'),
+	(3, 1, 'post_autosave_draft'),
 	(3, 1, 'post_reply_own'),
 	(3, 1, 'post_reply_any'),
 	(3, 1, 'post_unapproved_topics'),
@@ -235,6 +241,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 2, 'poll_vote'),
 	(0, 2, 'post_attachment'),
 	(0, 2, 'post_new'),
+	(0, 2, 'post_draft'),
+	(0, 2, 'post_autosave_draft'),
 	(0, 2, 'post_reply_any'),
 	(0, 2, 'post_reply_own'),
 	(0, 2, 'post_unapproved_topics'),
@@ -247,6 +255,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 2, 'view_attachments'),
 	(2, 2, 'moderate_board'),
 	(2, 2, 'post_new'),
+	(2, 2, 'post_draft'),
+	(2, 2, 'post_autosave_draft'),
 	(2, 2, 'post_reply_own'),
 	(2, 2, 'post_reply_any'),
 	(2, 2, 'post_unapproved_topics'),
@@ -280,6 +290,8 @@ VALUES (-1, 1, 'poll_view'),
 	(2, 2, 'view_attachments'),
 	(3, 2, 'moderate_board'),
 	(3, 2, 'post_new'),
+	(3, 2, 'post_draft'),
+	(3, 2, 'post_autosave_draft'),
 	(3, 2, 'post_reply_own'),
 	(3, 2, 'post_reply_any'),
 	(3, 2, 'post_unapproved_topics'),
@@ -331,6 +343,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 3, 'view_attachments'),
 	(2, 3, 'moderate_board'),
 	(2, 3, 'post_new'),
+	(2, 3, 'post_draft'),
+	(2, 3, 'post_autosave_draft'),
 	(2, 3, 'post_reply_own'),
 	(2, 3, 'post_reply_any'),
 	(2, 3, 'post_unapproved_topics'),
@@ -364,6 +378,8 @@ VALUES (-1, 1, 'poll_view'),
 	(2, 3, 'view_attachments'),
 	(3, 3, 'moderate_board'),
 	(3, 3, 'post_new'),
+	(3, 3, 'post_draft'),
+	(3, 3, 'post_autosave_draft'),
 	(3, 3, 'post_reply_own'),
 	(3, 3, 'post_reply_any'),
 	(3, 3, 'post_unapproved_topics'),
@@ -405,6 +421,8 @@ VALUES (-1, 1, 'poll_view'),
 	(0, 4, 'view_attachments'),
 	(2, 4, 'moderate_board'),
 	(2, 4, 'post_new'),
+	(2, 4, 'post_draft'),
+	(2, 4, 'post_autosave_draft'),
 	(2, 4, 'post_reply_own'),
 	(2, 4, 'post_reply_any'),
 	(2, 4, 'post_unapproved_topics'),
@@ -438,6 +456,8 @@ VALUES (-1, 1, 'poll_view'),
 	(2, 4, 'view_attachments'),
 	(3, 4, 'moderate_board'),
 	(3, 4, 'post_new'),
+	(3, 4, 'post_draft'),
+	(3, 4, 'post_autosave_draft'),
 	(3, 4, 'post_reply_own'),
 	(3, 4, 'post_reply_any'),
 	(3, 4, 'post_unapproved_topics'),
@@ -1463,6 +1483,8 @@ VALUES (-1, 'search_posts'),
 	(0, 'profile_view_any'),
 	(0, 'pm_read'),
 	(0, 'pm_send'),
+	(0, 'pm_draft'),
+	(0, 'pm_autosave_draft'),
 	(0, 'calendar_view'),
 	(0, 'view_stats'),
 	(0, 'who_view'),
@@ -1480,6 +1502,8 @@ VALUES (-1, 'search_posts'),
 	(2, 'profile_view_any'),
 	(2, 'pm_read'),
 	(2, 'pm_send'),
+	(2, 'pm_draft'),
+	(2, 'pm_autosave_draft'),
 	(2, 'calendar_view'),
 	(2, 'view_stats'),
 	(2, 'who_view'),
@@ -1616,7 +1640,8 @@ VALUES
 	(9, 0, 0, 1, 'w', 0, 'weekly_maintenance'),
 	(10, 0, 120, 1, 'd', 1, 'paid_subscriptions'),
 	(11, 0, 120, 1, 'd', 1, 'remove_temp_attachments'),
-	(12, 0, 180, 1, 'd', 1, 'remove_topic_redirect');
+	(12, 0, 180, 1, 'd', 1, 'remove_topic_redirect'),
+	(13, 0, 240, 1, 'd', 1, 'remove_old_drafts');
 	
 # --------------------------------------------------------
 
@@ -1743,6 +1768,7 @@ VALUES ('smfVersion', '{$smf_version}'),
 	('autoFixDatabase', '1'),
 	('allow_guestAccess', '1'),
 	('time_format', '{$default_time_format}'),
+	('number_format', '1234.00'),
 	('enableBBC', '1'),
 	('max_messageLength', '20000'),
 	('signature_settings', '1,300,0,0,0,0,0,0:'),
@@ -2014,3 +2040,27 @@ INSERT INTO {$db_prefix}topics
 	(id_topic, id_board, id_first_msg, id_last_msg, id_member_started, id_member_updated)
 VALUES (1, 1, 1, 1, 0, 0);
 # --------------------------------------------------------
+
+#
+# Table structure for table `user_drafts`
+#
+
+CREATE TABLE {$db_prefix}user_drafts (
+  id_draft int(10) unsigned NOT NULL auto_increment,
+  id_topic mediumint(8) unsigned NOT NULL default '0',
+  id_board smallint(5) unsigned NOT NULL default '0',
+  id_reply int(10) unsigned NOT NULL default '0',
+  type tinyint(4) NOT NULL default '0',
+  poster_time int(10) unsigned NOT NULL default '0',
+  id_member mediumint(8) unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled tinyint(4) NOT NULL default '1',
+  body mediumtext NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked tinyint(4) NOT NULL default '0',
+  is_sticky tinyint(4) NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox tinyint(4) NOT NULL default '0',
+  PRIMARY KEY (id_draft),
+  UNIQUE id_member (id_member, id_draft, type)
+) ENGINE=MyISAM;

+ 56 - 0
other/install_2-1_postgresql.sql

@@ -321,6 +321,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'poll_vote');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_attachment');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_unapproved_topics');
@@ -333,6 +335,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_unapproved_topics');
@@ -366,6 +370,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_unapproved_topics');
@@ -407,6 +413,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'poll_vote');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_attachment');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_unapproved_topics');
@@ -419,6 +427,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_unapproved_topics');
@@ -452,6 +462,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_unapproved_topics');
@@ -503,6 +515,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 3, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_unapproved_topics');
@@ -536,6 +550,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_unapproved_topics');
@@ -577,6 +593,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 4, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_unapproved_topics');
@@ -610,6 +628,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_unapproved_topics');
@@ -1913,6 +1933,8 @@ INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'profile_v
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'profile_view_any');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_read');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_send');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_draft');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_autosave_draft');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'calendar_view');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'view_stats');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'who_view');
@@ -1930,6 +1952,8 @@ INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'profile_v
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'profile_view_any');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_read');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_send');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_draft');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_autosave_draft');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'calendar_view');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'view_stats');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'who_view');
@@ -2108,6 +2132,7 @@ INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_r
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (10, 0, 120, 1, 'd', 1, 'paid_subscriptions');
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (11, 0, 120, 1, 'd', 1, 'remove_temp_attachments');
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (12, 0, 180, 1, 'd', 1, 'remove_topic_redirect');
+INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (12, 0, 240, 1, 'd', 1, 'remove_old_drafts');
 
 # --------------------------------------------------------
 
@@ -2232,6 +2257,7 @@ INSERT INTO {$db_prefix}settings (variable, value) VALUES ('edit_disable_time',
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('autoFixDatabase', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('allow_guestAccess', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('time_format', '{$default_time_format}');
+INSERT INTO {$db_prefix}settings (variable, value) VALUES ('number_format', '1234.00');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('enableBBC', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('max_messageLength', '20000');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('signature_settings', '1,300,0,0,0,0,0,0:');
@@ -2535,3 +2561,33 @@ INSERT INTO {$db_prefix}topics
 	(id_topic, id_board, id_first_msg, id_last_msg, id_member_started, id_member_updated)
 VALUES (1, 1, 1, 1, 0, 0);
 # --------------------------------------------------------
+
+
+#
+# Table structure for table `user_drafts`
+#
+
+CREATE TABLE {$db_prefix}user_drafts (
+  id_draft int unsigned NOT NULL auto_increment,
+  id_topic int unsigned NOT NULL default '0',
+  id_board smallint unsigned NOT NULL default '0',
+  id_reply int unsigned NOT NULL default '0',
+  type smallint NOT NULL default '0',
+  poster_time int unsigned NOT NULL default '0',
+  id_member int unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled smallint NOT NULL default '1',
+  body text NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked smallint NOT NULL default '0',
+  is_sticky smallint NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox smallint NOT NULL default '0',
+  PRIMARY KEY (id_draft)
+);
+
+#
+# Indexes for table `user_drafts`
+#
+
+CREATE UNIQUE INDEX {$db_prefix}id_member ON {$db_prefix}user_drafts (id_member, id_draft, type);

+ 55 - 0
other/install_2-1_sqlite.sql

@@ -159,6 +159,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'poll_vote');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_attachment');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'post_unapproved_topics');
@@ -171,6 +173,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 1, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'post_unapproved_topics');
@@ -204,6 +208,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 1, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 1, 'post_unapproved_topics');
@@ -245,6 +251,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'poll_vote');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_attachment');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'post_unapproved_topics');
@@ -257,6 +265,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 2, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'post_unapproved_topics');
@@ -290,6 +300,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 2, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 2, 'post_unapproved_topics');
@@ -341,6 +353,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 3, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'post_unapproved_topics');
@@ -374,6 +388,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 3, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 3, 'post_unapproved_topics');
@@ -415,6 +431,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (0, 4, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'post_unapproved_topics');
@@ -448,6 +466,8 @@ INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VAL
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (2, 4, 'view_attachments');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'moderate_board');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_new');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_draft');
+INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_autosave_draft');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_reply_own');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_reply_any');
 INSERT INTO {$db_prefix}board_permissions (id_group, id_profile, permission) VALUES (3, 4, 'post_unapproved_topics');
@@ -1589,6 +1609,8 @@ INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'profile_v
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'profile_view_any');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_read');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_send');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_draft');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'pm_autosave_draft');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'calendar_view');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'view_stats');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (0, 'who_view');
@@ -1606,6 +1628,8 @@ INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'profile_v
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'profile_view_any');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_read');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_send');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_draft');
+INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'pm_autosave_draft');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'calendar_view');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'view_stats');
 INSERT INTO {$db_prefix}permissions (id_group, permission) VALUES (2, 'who_view');
@@ -1759,6 +1783,7 @@ INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_r
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (10, 0, 120, 1, 'd', 1, 'paid_subscriptions');
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (11, 0, 120, 1, 'd', 1, 'remove_temp_attachments');
 INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (12, 0, 180, 1, 'd', 1, 'remove_topic_redirect');
+INSERT INTO {$db_prefix}scheduled_tasks	(id_task, next_time, time_offset, time_regularity, time_unit, disabled, task) VALUES (12, 0, 240, 1, 'd', 1, 'remove_old_drafts');
 COMMIT;
 
 # --------------------------------------------------------
@@ -1885,6 +1910,7 @@ INSERT INTO {$db_prefix}settings (variable, value) VALUES ('edit_disable_time',
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('autoFixDatabase', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('allow_guestAccess', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('time_format', '{$default_time_format}');
+INSERT INTO {$db_prefix}settings (variable, value) VALUES ('number_format', '1234.00');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('enableBBC', '1');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('max_messageLength', '20000');
 INSERT INTO {$db_prefix}settings (variable, value) VALUES ('signature_settings', '1,300,0,0,0,0,0,0:');
@@ -2170,3 +2196,32 @@ INSERT INTO {$db_prefix}topics
 	(id_topic, id_board, id_first_msg, id_last_msg, id_member_started, id_member_updated)
 VALUES (1, 1, 1, 1, 0, 0);
 # --------------------------------------------------------
+
+#
+# Table structure for table `user_drafts`
+#
+
+CREATE TABLE {$db_prefix}user_drafts (
+  id_draft int unsigned NOT NULL auto_increment,
+  id_topic int unsigned NOT NULL default '0',
+  id_board smallint unsigned NOT NULL default '0',
+  id_reply int unsigned NOT NULL default '0',
+  type smallint NOT NULL default '0',
+  poster_time int unsigned NOT NULL default '0',
+  id_member int unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled smallint NOT NULL default '1',
+  body text NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked smallint NOT NULL default '0',
+  is_sticky smallint NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox smallint NOT NULL default '0',
+  PRIMARY KEY (id_draft)
+);
+
+#
+# Indexes for table `user_drafts`
+#
+
+CREATE UNIQUE INDEX {$db_prefix}id_member ON {$db_prefix}user_drafts (id_member, id_draft, type);

+ 77 - 0
other/upgrade_2-1_mysql.sql

@@ -165,6 +165,10 @@ INSERT INTO {$db_prefix}scheduled_tasks
 	(next_time, time_offset, time_regularity, time_unit, disabled, task)
 VALUES
 	(0, 180, 1, 'd', 0, 'remove_topic_redirect');
+INSERT INTO {$db_prefix}scheduled_tasks
+	(next_time, time_offset, time_regularity, time_unit, disabled, task)
+VALUES
+	(0, 240, 1, 'd', 0, 'remove_old_drafts');
 ---#
 
 /******************************************************************************/
@@ -190,3 +194,76 @@ CHANGE body body mediumtext NOT NULL;
 ALTER TABLE {$db_prefix}membergroups
 CHANGE `stars` `icons` varchar(255) NOT NULL DEFAULT '';
 ---#
+
+/******************************************************************************/
+--- Adding support for drafts
+/******************************************************************************/
+---# Creating draft table
+CREATE TABLE IF NOT EXISTS {$db_prefix}user_drafts (
+  id_draft int(10) unsigned NOT NULL auto_increment,
+  id_topic mediumint(8) unsigned NOT NULL default '0',
+  id_board smallint(5) unsigned NOT NULL default '0',
+  id_reply int(10) unsigned NOT NULL default '0',
+  type tinyint(4) NOT NULL default '0',
+  poster_time int(10) unsigned NOT NULL default '0',
+  id_member mediumint(8) unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled tinyint(4) NOT NULL default '1',
+  body mediumtext NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked tinyint(4) NOT NULL default '0',
+  is_sticky tinyint(4) NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox tinyint(4) NOT NULL default '0',
+  PRIMARY KEY id_draft(id_draft),
+  UNIQUE id_member (id_member, id_draft, type),
+) ENGINE=MyISAM{$db_collation};
+---#
+
+---# Adding draft permissions...
+---{
+// We cannot do this twice
+if (@$modSettings['smfVersion'] < '2.1')
+{
+	// Anyone who can currently post unapproved topics we assume can create drafts as well ...
+	$request = upgrade_query("
+		SELECT id_group, id_board, add_deny, permission
+		FROM {$db_prefix}board_permissions
+		WHERE permission = 'post_unapproved_topics'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], $row[id_board], 'post_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], $row[id_board], 'post_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}board_permissions
+				(id_group, id_board, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+				
+	// Next we find people who can send PM's, and assume they can save pm_drafts as well
+	$request = upgrade_query("
+		SELECT id_group, add_deny, permission
+		FROM {$db_prefix}permissions
+		WHERE permission = 'pm_send'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], 'pm_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], 'pm_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}permissions
+				(id_group, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+}
+---}
+---#

+ 75 - 0
other/upgrade_2-1_postgresql.sql

@@ -219,6 +219,10 @@ INSERT INTO {$db_prefix}scheduled_tasks
 	(next_time, time_offset, time_regularity, time_unit, disabled, task)
 VALUES
 	(0, 180, 1, 'd', 0, 'remove_topic_redirect');
+INSERT INTO {$db_prefix}scheduled_tasks
+	(next_time, time_offset, time_regularity, time_unit, disabled, task)
+VALUES
+	(0, 240, 1, 'd', 0, 'remove_old_drafts');
 ---#
 
 /******************************************************************************/
@@ -242,3 +246,74 @@ upgrade_query("
 	CHANGE `stars` `icons` varchar(255) NOT NULL DEFAULT ''");
 ---}
 ---#
+
+/******************************************************************************/
+--- Adding support for drafts
+/******************************************************************************/
+---# Creating drafts table.
+CREATE TABLE {$db_prefix}user_drafts (
+  id_draft int unsigned NOT NULL auto_increment,
+  id_topic int unsigned NOT NULL default '0',
+  id_board smallint unsigned NOT NULL default '0',
+  id_reply int unsigned NOT NULL default '0',
+  type smallint NOT NULL default '0',
+  poster_time int unsigned NOT NULL default '0',
+  id_member int unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled smallint NOT NULL default '1',
+  body text NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked smallint NOT NULL default '0',
+  is_sticky smallint NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox smallint NOT NULL default '0',
+  PRIMARY KEY (id_draft)
+---#
+
+---# Adding draft permissions...
+---{
+// We cannot do this twice
+if (@$modSettings['smfVersion'] < '2.1')
+{
+	// Anyone who can currently post unapproved topics we assume can create drafts as well ...
+	$request = upgrade_query("
+		SELECT id_group, id_board, add_deny, permission
+		FROM {$db_prefix}board_permissions
+		WHERE permission = 'post_unapproved_topics'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], $row[id_board], 'post_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], $row[id_board], 'post_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}board_permissions
+				(id_group, id_board, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+				
+	// Next we find people who can send PM's, and assume they can save pm_drafts as well
+	$request = upgrade_query("
+		SELECT id_group, add_deny, permission
+		FROM {$db_prefix}permissions
+		WHERE permission = 'pm_send'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], 'pm_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], 'pm_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}permissions
+				(id_group, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+}
+---}
+---#

+ 75 - 0
other/upgrade_2-1_sqlite.sql

@@ -186,6 +186,10 @@ INSERT INTO {$db_prefix}scheduled_tasks
 	(next_time, time_offset, time_regularity, time_unit, disabled, task)
 VALUES
 	(0, 180, 1, 'd', 0, 'remove_topic_redirect');
+INSERT INTO {$db_prefix}scheduled_tasks
+	(next_time, time_offset, time_regularity, time_unit, disabled, task)
+VALUES
+	(0, 240, 1, 'd', 0, 'remove_old_drafts');
 ---#
 
 /******************************************************************************/
@@ -218,3 +222,74 @@ upgrade_query("
 	CHANGE `stars` `icons` varchar(255) NOT NULL DEFAULT ''");
 ---}
 ---#
+
+/******************************************************************************/
+--- Adding support for drafts
+/******************************************************************************/
+---# Creating drafts table.
+CREATE TABLE {$db_prefix}user_drafts (
+  id_draft int unsigned NOT NULL auto_increment,
+  id_topic int unsigned NOT NULL default '0',
+  id_board smallint unsigned NOT NULL default '0',
+  id_reply int unsigned NOT NULL default '0',
+  type smallint NOT NULL default '0',
+  poster_time int unsigned NOT NULL default '0',
+  id_member int unsigned NOT NULL default '0',
+  subject varchar(255) NOT NULL default '',
+  smileys_enabled smallint NOT NULL default '1',
+  body text NOT NULL,
+  icon varchar(16) NOT NULL default 'xx',
+  locked smallint NOT NULL default '0',
+  is_sticky smallint NOT NULL default '0',
+  to_list varchar(255) NOT NULL default '',
+  outbox smallint NOT NULL default '0',
+  PRIMARY KEY (id_draft)
+---#
+
+---# Adding draft permissions...
+---{
+// We cannot do this twice
+if (@$modSettings['smfVersion'] < '2.1')
+{
+	// Anyone who can currently post unapproved topics we assume can create drafts as well ...
+	$request = upgrade_query("
+		SELECT id_group, id_board, add_deny, permission
+		FROM {$db_prefix}board_permissions
+		WHERE permission = 'post_unapproved_topics'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], $row[id_board], 'post_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], $row[id_board], 'post_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}board_permissions
+				(id_group, id_board, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+				
+	// Next we find people who can send PM's, and assume they can save pm_drafts as well
+	$request = upgrade_query("
+		SELECT id_group, add_deny, permission
+		FROM {$db_prefix}permissions
+		WHERE permission = 'pm_send'");
+	$inserts = array();
+	while ($row = mysql_fetch_assoc($request))
+	{
+		$inserts[] = "($row[id_group], 'pm_draft', $row[add_deny])";
+		$inserts[] = "($row[id_group], 'pm_autosave_draft', $row[add_deny])";
+	}
+	mysql_free_result($request);
+
+	if (!empty($inserts))
+		upgrade_query("
+			INSERT IGNORE INTO {$db_prefix}permissions
+				(id_group, permission, add_deny)
+			VALUES
+				" . implode(',', $inserts));
+}
+---}
+---#