! The Short Straw: my last commit for SimpleDesk. This covers an almost entirely functional search facility for tickets, the only thing it's missing is a little bit of polish, a lot of testing and page numbers - you can't paginate through search results but other than that, it's working as far as I can tell. Note that the search setup is designed explicitly not to receive starting places but page numbers, and expressly via POST, rather than accepting anything via GET. This means the usual page index cannot be used. (The reason for this is to avoid server hammering. You can trivially get search engines to hammer a site and think it's legitimate by giving them a search to perform by URL and let them index it, so instead you must expressly use a POST rather than a bare URL.)
gruffen

gruffen commited on 2011-08-25 09:33:26
Showing 15 changed files, with 1589 additions and 73 deletions.

... ...
@@ -37,6 +37,8 @@ if (file_exists(dirname(__FILE__) . '/SSI.php') && !defined('SMF'))
37 37
 	require_once(dirname(__FILE__) . '/SSI.php');
38 38
 elseif (!defined('SMF')) // If we are outside SMF and can't find SSI.php, then throw an error
39 39
 	die('<b>Error:</b> Cannot install - please verify you put this file in the same place as SMF\'s SSI.php.');
40
+elseif (@version_compare(PHP_VERSION, '4.3.0', '<'))
41
+	die('<b>Error:</b> SimpleDesk 2.0 requires PHP 4.3.0 to be installed on your server.');
40 42
 
41 43
 if (SMF == 'SSI')
42 44
 	db_extend('packages');
... ...
@@ -536,6 +538,38 @@ $tables[] = array(
536 538
 	'error' => 'fatal',
537 539
 	'parameters' => array(),
538 540
 );
541
+$tables[] = array(
542
+	'table_name' => '{db_prefix}helpdesk_search_ticket_words',
543
+	'columns' => array(
544
+		db_field('id_word', 'bigint'),
545
+		db_field('id_msg', 'int'),
546
+	),
547
+	'indexes' => array(
548
+		array(
549
+			'columns' => array('id_word', 'id_msg'),
550
+			'type' => 'primary',
551
+		),
552
+	),
553
+	'if_exists' => 'ignore',
554
+	'error' => 'fatal',
555
+	'parameters' => array(),
556
+);
557
+$tables[] = array(
558
+	'table_name' => '{db_prefix}helpdesk_search_subject_words',
559
+	'columns' => array(
560
+		db_field('id_word', 'bigint'),
561
+		db_field('id_ticket', 'int'),
562
+	),
563
+	'indexes' => array(
564
+		array(
565
+			'columns' => array('id_word', 'id_ticket'),
566
+			'type' => 'primary',
567
+		),
568
+	),
569
+	'if_exists' => 'ignore',
570
+	'error' => 'fatal',
571
+	'parameters' => array(),
572
+);
539 573
 
540 574
 // Oh joy, we've now made it to extra rows...
541 575
 $rows = array();
... ...
@@ -659,6 +693,12 @@ if (!empty($new_dept))
659 693
 	);
660 694
 }
661 695
 
696
+// Do we need to flag that a new search index is needed? If there are any pre-existing tickets, we will...
697
+$query = $smcFunc['db_query']('', 'SELECT COUNT(*) FROM {db_prefix}helpdesk_tickets');
698
+list($count) = $smcFunc['db_fetch_row']($query);
699
+if (!empty($count))
700
+	updateSettings(array('shd_new_search_index' => 1));
701
+
662 702
 // If we're updating an existing install, we need to make sure there is a normalised value in the last_updated column.
663 703
 $smcFunc['db_query']('', '
664 704
 	UPDATE {db_prefix}helpdesk_tickets AS hdt, {db_prefix}helpdesk_ticket_replies AS hdtr
... ...
@@ -720,6 +760,14 @@ function db_field($name, $type, $size = 0, $unsigned = true, $auto = false)
720 760
 			'unsigned' => $unsigned,
721 761
 			'null' => false,
722 762
 		),
763
+		'bigint' => array(
764
+			'auto' => $auto,
765
+			'type' => 'bigint',
766
+			'default' => 0,
767
+			'size' => 21,
768
+			'unsigned' => $unsigned,
769
+			'null' => false,
770
+		),
723 771
 	);
724 772
 
725 773
 	$field = $fields[$type];
... ...
@@ -168,6 +168,7 @@ $txt['shd_welcome'] = 'Welcome, %s!';
168 168
 $txt['shd_go'] = 'Go!';
169 169
 $txt['shd_go_to_ticket'] = 'Go to ticket';
170 170
 $txt['shd_options'] = 'Options';
171
+$txt['shd_search_menu'] = 'Search';
171 172
 // The strings that go into the menu...
172 173
 $txt['shd_admin_info'] = 'Information';
173 174
 $txt['shd_admin_options'] = 'Options';
... ...
@@ -442,4 +443,38 @@ $txt['shd_ticket_notify_me_never'] = 'You have turned off all notifications for
442 443
 $txt['shd_ticket_notify_me_never_on'] = 'Turn off notifications';
443 444
 $txt['shd_ticket_notify_me_never_off'] = 'Turn on notifications';
444 445
 
446
+// Searching
447
+$txt['shd_search_warning_nonadmin'] = 'The search facility may not list all available tickets; it is currently being investigated.';
448
+$txt['shd_search_warning_admin'] = 'The search facility requires that its index be rebuilt. You can achieve this from the Maintenance option, in the Helpdesk area, in the administration panel.';
449
+$txt['shd_search'] = 'Search Tickets';
450
+$txt['shd_search_results'] = 'Search Tickets - Results';
451
+$txt['shd_search_text'] = 'Words you are looking for:';
452
+$txt['shd_search_match'] = 'What should be matched?';
453
+$txt['shd_search_match_all'] = 'Match all words supplied';
454
+$txt['shd_search_match_any'] = 'Match any words supplied';
455
+$txt['shd_search_scope'] = 'Include which types of tickets:';
456
+$txt['shd_search_scope_open'] = 'Open tickets';
457
+$txt['shd_search_scope_closed'] = 'Closed tickets';
458
+$txt['shd_search_scope_recycle'] = 'Items in the recycle bin';
459
+$txt['shd_search_result_ticket'] = 'Ticket %1$s';
460
+$txt['shd_search_result_reply'] = 'Reply to ticket %1$s';
461
+$txt['shd_search_last_updated'] = 'Last updated:';
462
+$txt['shd_search_ticket_opened_by'] = 'Ticket opened by:';
463
+$txt['shd_search_ticket_replied_by'] = 'Ticket replied to by:';
464
+$txt['shd_search_dept'] = 'Search in which department(s):';
465
+
466
+$txt['shd_search_urgency'] = 'Include which levels of urgency:';
467
+
468
+$txt['shd_search_where'] = 'Which items to search:';
469
+$txt['shd_search_where_tickets'] = 'The bodies of tickets';
470
+$txt['shd_search_where_replies'] = 'The replies in tickets';
471
+$txt['shd_search_where_subjects'] = 'Ticket subjects';
472
+
473
+$txt['shd_search_ticket_starter'] = 'Tickets started by:';
474
+$txt['shd_search_ticket_assignee'] = 'Tickets assigned to:';
475
+$txt['shd_search_ticket_named_person'] = 'Type in the name of the person(s) you are interested in.';
476
+
477
+$txt['shd_search_no_results'] = 'No results were found with the given criteria. You may wish to go back and try altering your search criteria.';
478
+$txt['shd_search_criteria'] = 'Search Criteria:';
479
+$txt['shd_search_excluded'] = 'If every possible option was selected, it has not been included in the above (e.g. if all possible levels of urgency were ticked, it is not stated above, so you can concentrate on what is specific to your search)';
445 480
 ?>
446 481
\ No newline at end of file
... ...
@@ -558,6 +558,18 @@ $txt['shd_maint_first_last'] = '%1$d ticket(s) had incorrect messages flagged fo
558 558
 $txt['shd_maint_status'] = '%1$d ticket(s) had the wrong status set for them. All have been rectified.';
559 559
 $txt['shd_maint_starter_updater'] = '%1$d ticket(s) had the wrong user listed as the person who opened the ticket or the last person to update the ticket. All have been rectified.';
560 560
 $txt['shd_maint_invalid_dept'] = '%1$d ticket(s) were listed as being in departments that do not exist, all were moved to a new department entitled "Recovered Tickets".';
561
+
562
+$txt['shd_maint_search_settings'] = 'Search Settings';
563
+$txt['shd_maint_search_settings_desc'] = 'This page allows you to configure how ticket searching may be performed, and if necessary, rebuild the index used to perform searching.';
564
+$txt['shd_maint_rebuild_index'] = 'Rebuild the Search Index';
565
+$txt['shd_maint_rebuild_index_desc'] = 'If you have existing tickets that were around prior to the search facility being provided, or you alter the settings below, you will <strong>need</strong> to rebuild the index after. The index is what is physically used to search, and if the physical index setup is different to how searches are made, you will find searching very unrealiable.<br /><strong>Important:</strong> Building the search index is a very intensive task. It will take a while to carry out, during which time please leave this window open.';
566
+$txt['shd_maint_search_settings_warning'] = 'If you alter these settings, you will need to rebuild the search index.';
567
+$txt['shd_search_min_size'] = 'Minimum number of letters to be considered a word (3-15)';
568
+$txt['shd_search_max_size'] = 'Maximum number of letters to be considered a word (3-15)';
569
+$txt['shd_search_prefix_size'] = 'Minimum number of letters to use for prefix searching<div class="smalltext">(0 = disabled)</div>';
570
+$txt['shd_search_prefix_size_help'] = 'Prefix searching is where the index is built to allow for partial word matches. For example, searching for &quot;walk&quot; will return results such as &quot;walking&quot; or &quot;walked&quot;. It is disabled by default because it makes the index significantly bigger and searches do get slower as a consequence.';
571
+$txt['shd_search_charset'] = 'Characters to consider as valid parts of words to search.';
572
+$txt['shd_search_rebuilt'] = 'The search index has been rebuilt.';
561 573
 //@}
562 574
 
563 575
 /**
... ...
@@ -275,6 +275,12 @@ $txt['permissionname_shd_view_ip_own'] = 'Only their own';
275 275
 $txt['permissionname_shd_view_ip_any'] = 'Anyone\'s';
276 276
 //@}
277 277
 
278
+//! @name Search
279
+//@{
280
+$txt['permissionname_shd_search'] = 'Search tickets';
281
+$txt['permissionhelp_shd_search'] = 'This permission allows users to search tickets, it will be limited to tickets they can see.';
282
+//@}
283
+
278 284
 //! @name User profile access
279 285
 //@{
280 286
 $txt['permissionname_shd_view_profile'] = 'View helpdesk profiles';
... ...
@@ -35,7 +35,7 @@ if (!defined('SMF'))
35 35
 */
36 36
 function shd_admin_maint()
37 37
 {
38
-	global $context, $txt, $db_show_debug;
38
+	global $context, $txt, $db_show_debug, $settings;
39 39
 
40 40
 	// Right, if we're here, we really, really need to turn this off. Because anything we do from this page onwards hurts the log badly.
41 41
 	$db_show_debug = false;
... ...
@@ -45,14 +45,55 @@ function shd_admin_maint()
45 45
 	loadLanguage('ManageMaintenance');
46 46
 
47 47
 	$subactions = array(
48
-		'main' => 'shd_admin_maint_home',
49
-		'reattribute' => 'shd_admin_maint_reattribute',
50
-		'massdeptmove' => 'shd_admin_maint_massdeptmove',
51
-		'findrepair' => 'shd_admin_maint_findrepair',
48
+		'main' => array(
49
+			'function' => 'shd_admin_maint_home',
50
+			'icon' => 'maintenance.png',
51
+			'title' => $txt['shd_admin_maint'],
52
+		),
53
+		'reattribute' => array(
54
+			'function' => 'shd_admin_maint_reattribute',
55
+			'icon' => 'user.png',
56
+			'title' => $txt['shd_admin_maint_reattribute'],
57
+			'description' => $txt['shd_admin_maint_reattribute_desc'],
58
+		),
59
+		'massdeptmove' => array(
60
+			'function' => 'shd_admin_maint_massdeptmove',
61
+			'icon' => 'movedept.png',
62
+			'title' => $txt['shd_admin_maint_massdeptmove'],
63
+			'description' => $txt['shd_admin_maint_massdeptmove'],
64
+		),
65
+		'findrepair' => array(
66
+			'function' => 'shd_admin_maint_findrepair',
67
+			'icon' => 'find_repair.png',
68
+			'title' => $txt['shd_admin_maint_findrepair'],
69
+			'description' => $txt['shd_admin_maint_findrepair_desc'],
70
+		),
71
+		'search' => array(
72
+			'function' => 'shd_admin_maint_search',
73
+			'icon' => 'search.png',
74
+			'title' => $txt['shd_maint_search_settings'],
75
+		),
52 76
 	);
53 77
 
54 78
 	$_REQUEST['sa'] = isset($_REQUEST['sa']) && isset($subactions[$_REQUEST['sa']]) ? $_REQUEST['sa'] : 'main';
55
-	$subactions[$_REQUEST['sa']]();
79
+
80
+	$context[$context['admin_menu_name']]['tab_data'] = array(
81
+		'title' => '<img src="' . $settings['default_theme_url'] . '/images/simpledesk/' . $subactions[$_REQUEST['sa']]['icon'] . '" class="icon" alt="*" />' . $subactions[$_REQUEST['sa']]['title'],
82
+		'description' => $txt['shd_admin_options_desc'],
83
+		'tabs' => array(
84
+			'main' => array(
85
+				'description' => $txt['shd_admin_maint_desc'],
86
+			),
87
+			'search' => array(
88
+				'description' => $txt['shd_maint_search_settings_desc'],
89
+			),
90
+		),
91
+	);
92
+
93
+	// We need to fix the descriptions just in case.
94
+	if (isset($subactions[$_REQUEST['sa']]['description']))
95
+		$context[$context['admin_menu_name']]['tab_data']['tabs']['main']['description'] = $subactions[$_REQUEST['sa']]['description'];
96
+	$subactions[$_REQUEST['sa']]['function']();
56 97
 }
57 98
 
58 99
 function shd_admin_maint_home()
... ...
@@ -521,7 +562,7 @@ function shd_maint_deleted()
521 562
 	{
522 563
 		// More to do, call back - and provide the subtitle
523 564
 		$context['continue_post_data'] .= '<input type="hidden" name="step" value="' . $context['step'] . '" />
524
-		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '">';
565
+		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '" />';
525 566
 		$context['substep_enabled'] = true;
526 567
 		$context['substep_title'] = $txt['shd_admin_maint_findrepair_status'];
527 568
 		$context['substep_continue_percent'] = round(100 * $_REQUEST['start'] / $ticket_count);
... ...
@@ -612,7 +653,7 @@ function shd_maint_first_last()
612 653
 	{
613 654
 		// More to do, call back - and provide the subtitle
614 655
 		$context['continue_post_data'] .= '<input type="hidden" name="step" value="' . $context['step'] . '" />
615
-		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '">';
656
+		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '" />';
616 657
 		$context['substep_enabled'] = true;
617 658
 		$context['substep_title'] = $txt['shd_admin_maint_findrepair_firstlast'];
618 659
 		$context['substep_continue_percent'] = round(100 * $_REQUEST['start'] / $ticket_count);
... ...
@@ -703,7 +744,7 @@ function shd_maint_starter_updater()
703 744
 	{
704 745
 		// More to do, call back - and provide the subtitle
705 746
 		$context['continue_post_data'] .= '<input type="hidden" name="step" value="' . $context['step'] . '" />
706
-		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '">';
747
+		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '" />';
707 748
 		$context['substep_enabled'] = true;
708 749
 		$context['substep_title'] = $txt['shd_admin_maint_findrepair_starterupdater'];
709 750
 		$context['substep_continue_percent'] = round(100 * $_REQUEST['start'] / $ticket_count);
... ...
@@ -790,7 +831,7 @@ function shd_maint_status()
790 831
 	{
791 832
 		// More to do, call back - and provide the subtitle
792 833
 		$context['continue_post_data'] .= '<input type="hidden" name="step" value="' . $context['step'] . '" />
793
-		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '">';
834
+		<input type="hidden" name="start" value="' . $_REQUEST['start'] . '" />';
794 835
 		$context['substep_enabled'] = true;
795 836
 		$context['substep_title'] = $txt['shd_admin_maint_findrepair_firstlast'];
796 837
 		$context['substep_continue_percent'] = round(100 * $_REQUEST['start'] / $ticket_count);
... ...
@@ -863,4 +904,223 @@ function shd_maint_clean_cache()
863 904
 	redirectexit('action=admin;area=helpdesk_maint;sa=findrepair;done;' . $context['session_var'] . '=' . $context['session_id']);
864 905
 }
865 906
 
907
+function shd_admin_maint_search()
908
+{
909
+	global $context, $txt, $modSettings, $sourcedir, $smcFunc;
910
+
911
+	$context['sub_template'] = 'shd_admin_maint_search';
912
+	$context['page_title'] = $txt['shd_admin_maint'];
913
+
914
+	checkSession('request');
915
+
916
+	// Reset the defaults if they're not set.
917
+	if (empty($modSettings['shd_search_charset']))
918
+		$modSettings['shd_search_charset'] = '0..9, A..Z, a..z, &, ~';
919
+
920
+	$modSettings['shd_search_min_size'] = !empty($modSettings['shd_search_min_size']) ? $modSettings['shd_search_min_size'] : 3;
921
+	$modSettings['shd_search_max_size'] = !empty($modSettings['shd_search_max_size']) ? $modSettings['shd_search_max_size'] : 8;
922
+	$modSettings['shd_search_prefix_size'] = !empty($modSettings['shd_search_prefix_size']) ? $modSettings['shd_search_prefix_size'] : 0;
923
+
924
+	// Are we doing some fancy work?
925
+	if (isset($_REQUEST['rebuild']))
926
+	{
927
+		require_once($sourcedir . '/sd_source/Subs-SimpleDeskSearch.php');
928
+		// How many tickets are there?
929
+		$query = $smcFunc['db_query']('', '
930
+			SELECT COUNT(id_ticket)
931
+			FROM {db_prefix}helpdesk_tickets');
932
+		list($total) = $smcFunc['db_fetch_row']($query);
933
+
934
+		// Where are we starting?
935
+		$start = isset($_POST['start']) ? (int) $_POST['start'] : 0;
936
+
937
+		// Get the ids we need to do.
938
+		$per_inst = 10;
939
+		$tickets = array();
940
+		$query = $smcFunc['db_query']('', '
941
+			SELECT id_ticket, subject
942
+			FROM {db_prefix}helpdesk_tickets
943
+			ORDER BY id_ticket ASC
944
+			LIMIT {int:start}, {int:limit}',
945
+			array(
946
+				'start' => $start,
947
+				'limit' => $per_inst,
948
+			)
949
+		);
950
+		while ($row = $smcFunc['db_fetch_assoc']($query))
951
+			$tickets[$row['id_ticket']] = $row['subject'];
952
+		$smcFunc['db_free_result']($query);
953
+
954
+		// Nothing to do?
955
+		if ($start >= $total || empty($tickets))
956
+		{
957
+			// Make sure we flag the index as built, then leave.
958
+			updateSettings(
959
+				array(
960
+					'shd_new_search_index' => 0,
961
+				)
962
+			);
963
+			redirectexit('action=admin;area=helpdesk_maint;sa=search;rebuilddone;' . $context['session_var'] . '=' . $context['session_id']);
964
+		}
965
+
966
+		// OK, let's get cracking. First, remove the relevant tickets from the subject index.
967
+		$smcFunc['db_query']('', '
968
+			DELETE FROM {db_prefix}helpdesk_search_subject_words
969
+			WHERE id_ticket IN ({array_int:tickets})',
970
+			array(
971
+				'tickets' => array_keys($tickets),
972
+			)
973
+		);
974
+
975
+		// Now, figure out the new term index for the subjects.
976
+		$rows_to_insert = array();
977
+		foreach ($tickets as $id_ticket => $subject)
978
+		{
979
+			$tokens = shd_tokeniser($subject);
980
+			foreach ($tokens as $token)
981
+				$rows_to_insert[] = array($token, $id_ticket);
982
+		}
983
+
984
+		// And add to the database.
985
+		if (!empty($rows_to_insert))
986
+			$smcFunc['db_insert']('replace',
987
+				'{db_prefix}helpdesk_search_subject_words',
988
+				array('id_word' => 'string', 'id_ticket' => 'int'),
989
+				$rows_to_insert,
990
+				array('id_word', 'id_ticket')
991
+			);
992
+
993
+		// Now for the slightly... substantially more expensive part: messages. We query for all the messages in a ticket, then query to
994
+		// insert all the terms for each message. Expensive since it means a lot of queries but it means we don't risk hitting the query
995
+		// packet limit which could really break things. Besides, this IS a maintenance area, not something you're going to do that often.
996
+		foreach ($tickets as $id_ticket => $subject)
997
+		{
998
+			$rows_to_insert = array();
999
+			$query = $smcFunc['db_query']('', '
1000
+				SELECT id_msg, body
1001
+				FROM {db_prefix}helpdesk_ticket_replies
1002
+				WHERE id_ticket = {int:ticket}',
1003
+				array(
1004
+					'ticket' => $id_ticket,
1005
+				)
1006
+			);
1007
+			$msg_list = array();
1008
+			while ($row = $smcFunc['db_fetch_assoc']($query))
1009
+			{
1010
+				$msg_list[] = $row['id_msg'];
1011
+				$tokens = shd_tokeniser($row['body']);
1012
+				foreach ($tokens as $token)
1013
+					$rows_to_insert[] = array($token, $row['id_msg']);
1014
+			}
1015
+			$smcFunc['db_free_result']($query);
1016
+
1017
+			// Just before we insert, prune the old stuff. No point querying the message list twice.
1018
+			$smcFunc['db_query']('', '
1019
+				DELETE FROM {db_prefix}helpdesk_search_ticket_words
1020
+				WHERE id_msg IN ({array_int:msgs})',
1021
+				array(
1022
+					'msgs' => $msg_list,
1023
+				)
1024
+			);
1025
+
1026
+			if (!empty($rows_to_insert))
1027
+				$smcFunc['db_insert']('replace',
1028
+					'{db_prefix}helpdesk_search_ticket_words',
1029
+					array('id_word' => 'string', 'id_msg' => 'int'),
1030
+					$rows_to_insert,
1031
+					array('id_word', 'id_msg')
1032
+				);
1033
+		}
1034
+
1035
+		// Set up for calling back.
1036
+		$start += $per_inst;
1037
+		$pc_done = round($start / $total * 100);
1038
+		if ($pc_done > 100)
1039
+			$pc_done = 100;
1040
+
1041
+		$context['continue_countdown'] = 3;
1042
+		$context['sub_template'] = 'not_done';
1043
+		$context['continue_percent'] = $pc_done;
1044
+		$context['continue_get_data'] = '?action=admin;area=helpdesk_maint;sa=search;' . $context['session_var'] . '=' . $context['session_id'];
1045
+		$context['continue_post_data'] = '<input type="hidden" name="start" value="' . $start . '" />
1046
+		<input type="hidden" name="rebuild" value="1" />';
1047
+
1048
+		// Make SURE we never mess with the other settings.
1049
+		unset($_REQUEST['save']);
1050
+	}
1051
+
1052
+	// OK, the template will basically display itself, but in the meantime, do we need to do anything else like save new settings?
1053
+	if (isset($_REQUEST['save']))
1054
+	{
1055
+		$_POST['shd_search_min_size'] = isset($_POST['shd_search_min_size']) ? (int) $_POST['shd_search_min_size'] : 0;
1056
+		$_POST['shd_search_max_size'] = isset($_POST['shd_search_max_size']) ? (int) $_POST['shd_search_max_size'] : 0;
1057
+		$_POST['shd_search_prefix_size'] = isset($_POST['shd_search_prefix_size']) ? (int) $_POST['shd_search_prefix_size'] : 0;
1058
+
1059
+		// Force some realistic limits.
1060
+		if ($_POST['shd_search_min_size'] < 3)
1061
+			$_POST['shd_search_min_size'] = 3;
1062
+		elseif ($_POST['shd_search_min_size'] > 15)
1063
+			$_POST['shd_search_min_size'] = 15;
1064
+		
1065
+		if ($_POST['shd_search_max_size'] < $_POST['shd_search_min_size'])
1066
+			$_POST['shd_search_max_size'] = $_POST['shd_search_min_size'];
1067
+		elseif ($_POST['shd_search_max_size'] > 15)
1068
+			$_POST['shd_search_max_size'] = 15;
1069
+			
1070
+		if ($_POST['shd_search_prefix_size'] < 0)
1071
+			$_POST['shd_search_prefix_size'] = 0;
1072
+		elseif ($_POST['shd_search_prefix_size'] > 0 && $_POST['shd_search_prefix_size'] < $_POST['shd_search_min_size'])
1073
+			$_POST['shd_search_prefix_size'] = $_POST['shd_search_min_size'];
1074
+		elseif ($_POST['shd_search_prefix_size'] > $_POST['shd_search_max_size'])
1075
+			$_POST['shd_search_prefix_size'] = $_POST['shd_search_max_size'];
1076
+
1077
+		$normal_regex = shd_return_exclude_regex($modSettings['shd_search_charset']);
1078
+		if (empty($_POST['shd_search_charset']))
1079
+			$_POST['shd_search_charset'] = $modSettings['shd_search_charset'];
1080
+		$post_regex = shd_return_exclude_regex($_POST['shd_search_charset']);
1081
+		if (empty($post_regex))
1082
+			$post_regex = $normal_regex; // Nothing specified? Use what we have, then.
1083
+
1084
+		foreach (array('shd_search_min_size', 'shd_search_max_size', 'shd_search_prefix_size') as $item)
1085
+			if ($modSettings[$item] != $_POST[$item])
1086
+				$update = true;
1087
+
1088
+		if ($normal_regex != $post_regex)
1089
+			$update = true;
1090
+
1091
+		if (!empty($update))
1092
+			updateSettings(
1093
+				array(
1094
+					'shd_search_min_size' => $_POST['shd_search_min_size'],
1095
+					'shd_search_max_size' => $_POST['shd_search_max_size'],
1096
+					'shd_search_prefix_size' => $_POST['shd_search_prefix_size'],
1097
+					'shd_search_charset' => $_POST['shd_search_charset'],
1098
+					'shd_new_search_index' => 1,
1099
+				)
1100
+			);
1101
+	}
1102
+}
1103
+
1104
+// This uses the same methodology as Subs-SimpleDeskSearch.php's shd_search_charset routine.
1105
+function shd_return_exclude_regex($source)
1106
+{
1107
+	global $context;
1108
+
1109
+	$terms = explode(',', $source);
1110
+	$exclude_regex = '';
1111
+	foreach ($terms as $k => $v)
1112
+	{
1113
+		$v = trim($v);
1114
+		if (preg_match('~^(.)$~i' . ($context['utf8'] ? 'u' : ''), $v, $match)) // Single character
1115
+			$exclude_regex .= preg_quote($match[1], '~');
1116
+		elseif (preg_match('~^(.)\.\.(.)$~i' . ($context['utf8'] ? 'u' : ''), $v, $match)) // It's a ranged component.
1117
+			$exclude_regex .= preg_quote($match[1], '~') . '-' . preg_quote($match[2], '~');
1118
+	}
1119
+	if (empty($exclude_regex))
1120
+		$exclude_regex = '';
1121
+	else
1122
+		$exclude_regex = '~[^' . $exclude_regex . ']+~' . ($context['utf8'] ? 'u' : '');
1123
+
1124
+	return $exclude_regex;
1125
+}
866 1126
 ?>
867 1127
\ No newline at end of file
... ...
@@ -200,6 +200,11 @@ function shd_perma_delete()
200 200
 
201 201
 	checkSession('get');
202 202
 
203
+	// This is heavy duty stuff.
204
+	@set_time_limit(0);
205
+	if (is_callable('apache_reset_timeout'))
206
+		apache_reset_timeout();
207
+
203 208
 	// We have to have either a ticket or a reply to know what to delete (Or do you want me to drop your whole database? >:D)
204 209
 	if (empty($context['ticket_id']) && empty($_REQUEST['reply']))
205 210
 		fatal_lang_error('shd_no_ticket', false);
... ...
@@ -294,6 +299,23 @@ function shd_perma_delete()
294 299
 			)
295 300
 		);
296 301
 
302
+		// And search entries.
303
+		shd_db_query('', '
304
+			DELETE FROM {db_prefix}helpdesk_search_ticket_words
305
+			WHERE id_msg = ({array_int:msgs})',
306
+			array(
307
+				'msgs' => $msgs,
308
+			)
309
+		);
310
+
311
+		shd_db_query('', '
312
+			DELETE FROM {db_prefix}helpdesk_search_subject_words
313
+			WHERE id_ticket = {int:ticket}',
314
+			array(
315
+				'current_ticket' => $context['ticket_id'],
316
+			)
317
+		);
318
+
297 319
 		// And attachments... work out which attachments that is
298 320
 		$query = shd_db_query('', '
299 321
 			SELECT id_attach
... ...
@@ -392,6 +414,15 @@ function shd_perma_delete()
392 414
 			)
393 415
 		);
394 416
 
417
+		// And search entries.
418
+		shd_db_query('', '
419
+			DELETE FROM {db_prefix}helpdesk_search_ticket_words
420
+			WHERE id_msg = {int:reply}',
421
+			array(
422
+				'reply' => (int) $_REQUEST['reply'],
423
+			)
424
+		);
425
+
395 426
 		// Now to handle attachments
396 427
 		$query = shd_db_query('', '
397 428
 			SELECT id_attach
... ...
@@ -0,0 +1,510 @@
1
+<?php
2
+###############################################################
3
+#         Simple Desk Project - www.simpledesk.net            #
4
+###############################################################
5
+#       An advanced help desk modifcation built on SMF        #
6
+###############################################################
7
+#                                                             #
8
+#         * Copyright 2010 - SimpleDesk.net                   #
9
+#                                                             #
10
+#   This file and its contents are subject to the license     #
11
+#   included with this distribution, license.txt, which       #
12
+#   states that this software is New BSD Licensed.            #
13
+#   Any questions, please contact SimpleDesk.net              #
14
+#                                                             #
15
+###############################################################
16
+# SimpleDesk Version: 2.0 Anatidae                            #
17
+# File Info: SimpleDesk-Search.php / 2.0 Anatidae             #
18
+###############################################################
19
+
20
+/**
21
+ *	This file handles searching, providing the interface and handling.
22
+ *
23
+ *	@package source
24
+ *	@since 2.0
25
+*/
26
+
27
+if (!defined('SMF'))
28
+	die('Hacking attempt...');
29
+
30
+function shd_search()
31
+{
32
+	global $context, $smcFunc, $txt, $modSettings, $scripturl;
33
+
34
+	shd_is_allowed_to('shd_search', 0);
35
+
36
+	if (!empty($context['load_average']) && !empty($modSettings['loadavg_search']) && $context['load_average'] >= $modSettings['loadavg_search'])
37
+		fatal_lang_error('loadavg_search_disabled', false);
38
+
39
+	loadTemplate('sd_template/SimpleDesk-Search');
40
+
41
+	$visible_depts = shd_allowed_to('access_helpdesk', false);
42
+	$context['dept_list'] = array();
43
+	$query = $smcFunc['db_query']('', '
44
+		SELECT id_dept, dept_name
45
+		FROM {db_prefix}helpdesk_depts
46
+		WHERE id_dept IN ({array_int:depts})
47
+		ORDER BY dept_order',
48
+		array(
49
+			'depts' => $visible_depts,
50
+		)
51
+	);
52
+	while ($row = $smcFunc['db_fetch_assoc']($query))
53
+		$context['dept_list'][$row['id_dept']] = $row['dept_name'];
54
+	$smcFunc['db_free_result']($query);
55
+
56
+	$context['sub_template'] = 'search';
57
+	$context['page_title'] = $txt['shd_search'];
58
+
59
+	$context['linktree'][] = array(
60
+		'url' => $scripturl . '?action=helpdesk;sa=search',
61
+		'name' => $txt['shd_search'],
62
+	);
63
+}
64
+
65
+function shd_search2()
66
+{
67
+	global $context, $smcFunc, $txt, $modSettings, $scripturl, $sourcedir;
68
+
69
+	shd_is_allowed_to('shd_search', 0);
70
+
71
+	if (!empty($context['load_average']) && !empty($modSettings['loadavg_search']) && $context['load_average'] >= $modSettings['loadavg_search'])
72
+		fatal_lang_error('loadavg_search_disabled', false);
73
+
74
+	// No, no, no... this is a bit hard on the server, so don't you go prefetching it!
75
+	if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch')
76
+	{
77
+		ob_end_clean();
78
+		header('HTTP/1.1 403 Forbidden');
79
+		die;
80
+	}
81
+
82
+	// We will need this.
83
+	require_once($sourcedir . '/sd_source/Subs-SimpleDeskSearch.php');
84
+
85
+	loadTemplate('sd_template/SimpleDesk-Search');
86
+	$context['page_title'] = $txt['shd_search_results'];
87
+
88
+	$context['linktree'][] = array(
89
+		'name' => $txt['shd_search_results'],
90
+	);
91
+
92
+	$context['search_clauses'] = array('{query_see_ticket}');
93
+	$context['search_params'] = array();
94
+
95
+	// Departments first.
96
+	$visible_depts = shd_allowed_to('access_helpdesk', false);
97
+	$using_depts = array();
98
+	if (!empty($_POST['search_dept']) && is_array($_POST['search_dept']))
99
+		foreach ($_POST['search_dept'] as $dept)
100
+			if ((int) $dept > 0)
101
+				$using_depts[] = (int) $dept;
102
+	if (!empty($using_depts))
103
+		$using_depts = array_intersect($using_depts, $visible_depts);
104
+	// No departments? Can't really do a lot, sorry. Bye then.
105
+	if (empty($using_depts))
106
+		return $context['sub_template'] = 'search_no_results';
107
+
108
+	// Is the selected list the same size as the list we can see? If it is, theory says that means we're picking every department we can see and don't need to exclude it.
109
+	if (count($using_depts) != count($visible_depts))
110
+	{
111
+		$context['search_clauses'][] = 'hdt.id_dept IN ({array_int:visible_depts})';
112
+		$context['search_params']['visible_depts'] = $using_depts;
113
+
114
+		// Also, we need to get the department list for displaying, only if we can actually see multiple departments at all.
115
+		if ($context['shd_multi_dept'])
116
+		{
117
+			$query = $smcFunc['db_query']('', '
118
+				SELECT id_dept, dept_name
119
+				FROM {db_prefix}helpdesk_depts
120
+				WHERE id_dept IN ({array_int:dept_list})
121
+				ORDER BY dept_order',
122
+				array(
123
+					'dept_list' => $using_depts,
124
+				)
125
+			);
126
+			$context['search_dept_list'] = array();
127
+			while ($row = $smcFunc['db_fetch_assoc']($query))
128
+				$context['search_dept_list'][$row['id_dept']] = $row['dept_name'];
129
+			$smcFunc['db_free_result']($query);
130
+		}
131
+	}
132
+
133
+	// Ticket urgency
134
+	$using_urgency = array();
135
+	if (!empty($_POST['urgency']) && is_array($_POST['urgency']))
136
+		foreach ($_POST['urgency'] as $urgency)
137
+		{
138
+			$urgency = (int) $urgency;
139
+			if ($urgency >= 0 && $urgency <= 5) // All the currently defined urgencies
140
+				$using_urgency[] = $urgency;
141
+		}
142
+	if (empty($using_urgency))
143
+		return $context['sub_template'] = 'search_no_results';
144
+	else
145
+	{
146
+		$using_urgency = array_unique($using_urgency);
147
+		if (count($using_urgency) < 6)
148
+		{
149
+			// We have less than 6 selected urgencies, which means we actually need to filter on them, as opposed to if all 6 are selected when we don't.
150
+			$context['search_clauses'][] = 'hdt.urgency IN ({array_int:urgency})';
151
+			$context['search_params']['urgency'] = $using_urgency;
152
+		}
153
+	}
154
+
155
+	// Ticket scope
156
+	// All empty? If so, bye.
157
+	if (empty($_POST['scope_open']) && empty($_POST['scope_closed']) && empty($_POST['scope_recycle']))
158
+		return $context['sub_template'] = 'search_no_results';
159
+	// At least one empty? That way we have to do some filtering, you see. (All set means no filtering required.)
160
+	elseif (empty($_POST['scope_open']) || empty($_POST['scope_closed']) || empty($_POST['scope_recycle']))
161
+	{
162
+		$status = array();
163
+		if (!empty($_POST['scope_open']))
164
+			$status = array_merge($status, array(TICKET_STATUS_NEW, TICKET_STATUS_PENDING_STAFF, TICKET_STATUS_PENDING_USER, TICKET_STATUS_WITH_SUPERVISOR, TICKET_STATUS_ESCALATED));
165
+		if (!empty($_POST['scope_closed']))
166
+			$status = array_merge($status, array(TICKET_STATUS_CLOSED));
167
+		if (!empty($_POST['scope_recycle']))
168
+			$status = array_merge($status, array(TICKET_STATUS_DELETED));
169
+
170
+		$context['search_clauses'][] = 'hdt.status IN ({array_int:status})';
171
+		$context['search_params']['status'] = $status;
172
+
173
+		// That's ticket level status taken care of. We'll pick up recycled items in non recycled tickets separately since it's only relevant if you're actually searching text.
174
+	}
175
+
176
+	// Ticket starter
177
+	$starters = shd_get_named_people('starter');
178
+	if (!empty($starters))
179
+	{
180
+		$context['search_clauses'][] = 'hdt.id_member_started IN ({array_int:member_started})';
181
+		$context['search_params']['member_started'] = $starters;
182
+	}
183
+
184
+	// Ticket assigned to
185
+	$assignees = shd_get_named_people('assignee');
186
+	if (!empty($assignees))
187
+	{
188
+		$context['search_clauses'][] = 'hdt.id_member_assigned IN ({array_int:member_assigned})';
189
+		$context['search_params']['member_assigned'] = $assignees;
190
+	}
191
+
192
+	// Lastly, page number. We're doing something different to SMF's normal style here. Long and complicated, but there you go.
193
+	if (isset($_POST['page']))
194
+		$context['pagenum'] = (int) $_POST['page'];
195
+	if (empty($context['pagenum']) || $context['pagenum'] < 1)
196
+		$context['pagenum'] = 1;
197
+
198
+	$number_per_page = 20;
199
+
200
+	// OK, so are there any words? If not, execute this sucker the quick way and get out to the template quick.
201
+	$context['search_terms'] = !empty($_POST['search']) ? trim($_POST['search']) : '';
202
+
203
+	// Also, did we select some text but fail to select what it was searching in? If so, kick it out.
204
+	if (!empty($context['search_terms']) && empty($_POST['search_subjects']) && empty($_POST['search_tickets']) && empty($_POST['search_replies']))
205
+		return $context['sub_template'] = 'search_no_results';
206
+	elseif (!empty($context['search_terms']))
207
+	{
208
+		// We're using search terms, and we need to store the areas we're covering. Only makes sense if we're using terms though.
209
+		$context['search_params']['areas'] = array();
210
+		foreach (array('subjects', 'tickets', 'replies') as $area)
211
+			if (!empty($_POST['search_' . $area]))
212
+				$context['search_params']['areas'][$area] = true;
213
+
214
+		// While we're at it, see if we actually have any words to search for.
215
+		$tokens = shd_tokeniser($context['search_terms']);
216
+		$count_tokens = count($tokens);
217
+
218
+		// No actual words?
219
+		if ($count_tokens == 0)
220
+		{
221
+			$context['search_terms'] = '';
222
+			unset($context['search_params']['areas']);
223
+		}
224
+	}
225
+
226
+	// Spam me not!
227
+	if (empty($_SESSION['lastsearch']))
228
+		spamProtection('search');
229
+	else
230
+	{
231
+		list($temp_clauses, $temp_params, $temp_terms) = unserialize($_SESSION['lastsearch']);
232
+		if ($temp_clauses != $context['search_clauses'] || $temp_params != $context['search_params'] || $temp_terms != $context['search_terms'])
233
+			spamProtection('search');
234
+	}
235
+	$_SESSION['lastsearch'] = serialize(array($context['search_clauses'], $context['search_params'], $context['search_terms']));
236
+
237
+	$context['search_params']['start'] = ($context['pagenum'] - 1) * $number_per_page;
238
+	$context['search_params']['limit'] = $number_per_page;
239
+
240
+	if (empty($context['search_terms']))
241
+	{
242
+		// This is where it starts to get expensive, *sob*. We first have to query to get the number of applicable rows.
243
+		$query = shd_db_query('', '
244
+			SELECT COUNT(id_ticket)
245
+			FROM {db_prefix}helpdesk_tickets AS hdt
246
+			WHERE ' . implode(' AND ', $context['search_clauses']) . ' LIMIT 1000',
247
+			$context['search_params']
248
+		);
249
+		list($count) = $smcFunc['db_fetch_row']($query);
250
+		if ($count == 0)
251
+		{
252
+			$smcFunc['db_free_result']($query);
253
+			return $context['sub_template'] = 'search_no_results';
254
+		}
255
+		// OK, at least one result, awesome. Are we off the end of the list?
256
+		if ($context['search_params']['start'] > $count)
257
+		{
258
+			$context['search_params']['start'] = $count - ($count % $number_per_page);
259
+			$context['pagenum'] = ($context['search_params']['start'] / $number_per_page) + 1;
260
+			$context['num_results'] = $count);
261
+		}
262
+
263
+		$query = shd_db_query('', '
264
+			SELECT hdt.id_ticket, hdt.id_dept, hdd.dept_name, hdt.subject, hdt.urgency, hdt.private, hdt.last_updated, hdtr.body,
265
+				hdtr.smileys_enabled, hdtr.id_member AS id_member, IFNULL(mem.real_name, hdtr.poster_name) AS poster_name, hdtr.poster_time
266
+			FROM {db_prefix}helpdesk_tickets AS hdt
267
+				INNER JOIN {db_prefix}helpdesk_ticket_replies AS hdtr ON (hdt.id_first_msg = hdtr.id_msg)
268
+				INNER JOIN {db_prefix}helpdesk_depts AS hdd ON (hdt.id_dept = hdd.id_dept)
269
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = hdtr.id_member)
270
+			WHERE ' . implode(' AND ', $context['search_clauses']) . '
271
+			ORDER BY hdt.last_updated DESC
272
+			LIMIT {int:start}, {int:limit}',
273
+			$context['search_params']
274
+		);
275
+
276
+		$context['search_results'] = array();
277
+		$page_pos = $context['search_params']['start']; // e.g. 0 on page 1, 10 for page 2, the first item will be page_pos + 1, so ++ it before using it.
278
+		while ($row = $smcFunc['db_fetch_assoc']($query))
279
+		{
280
+			$row['result'] = ++$page_pos; // Increment first, then use.
281
+			$row['display_id'] = str_pad($row['id_ticket'], $modSettings['shd_zerofill'], '0', STR_PAD_LEFT);
282
+			$row['is_ticket'] = true; // If we're here, we're only handling tickets anyway. If we're searching text we will need to know if it was a ticket or reply though.
283
+			$row['dept_link'] = !$context['shd_multi_dept'] ? '' : '[<a href="' . $scripturl . '?action=helpdesk;sa=main;dept=' . $row['id_dept'] . '">' . $row['dept_name'] . '</a>] ';
284
+
285
+			$context['search_results'][] = $row;
286
+		}
287
+
288
+		return $context['sub_template'] = 'search_results';
289
+	}
290
+	else
291
+	{
292
+		$context['match_all'] = empty($_POST['searchtype']) || $_POST['searchtype'] == 'all';
293
+
294
+		// Then figure out what terms are being matched.
295
+		$matches = array(
296
+			'subjects' => array(),
297
+			'messages' => array(),
298
+			'id_msg' => array(),
299
+		);
300
+
301
+		// Doing subjects. Fetch all the instances that match and begin filtering as we go.
302
+		if (!empty($context['search_params']['areas']['subjects']))
303
+		{
304
+			$query = shd_db_query('', '
305
+				SELECT hdssw.id_word, hdt.id_first_msg
306
+				FROM {db_prefix}helpdesk_search_subject_words AS hdssw
307
+					INNER JOIN {db_prefix}helpdesk_tickets AS hdt ON (hdssw.id_ticket = hdt.id_ticket)
308
+				WHERE {query_see_ticket}
309
+					AND id_word IN ({array_string:tokens})',
310
+				array(
311
+					'tokens' => $tokens,
312
+				)
313
+			);
314
+			while ($row = $smcFunc['db_fetch_assoc']($query))
315
+				$matches['subjects'][$row['id_first_msg']][$row['id_word']] = true;
316
+			$smcFunc['db_free_result']($query);
317
+
318
+			// Now go through and figure out which tickets we're interested in keeping.
319
+			if ($context['match_all'])
320
+				foreach ($matches['subjects'] as $msg => $ticket_words)
321
+					if (count($ticket_words) != $count_tokens) // How many words did we match in this subject? If it isn't the number we're expecting, ditch it.
322
+						unset($matches['subjects'][$msg]);
323
+
324
+			// Now, we just have a list of tickets to play with. Let's put that together in a master list.
325
+			foreach ($matches['subjects'] as $msg => $ticket_words)
326
+				$matches['id_msg'][$msg] = true;
327
+
328
+			unset($matches['subjects']);
329
+		}
330
+
331
+		// Now we get the list of words that apply to tickets and replies. The process is different if we do one or both. Both, first.
332
+		if (!empty($context['search_params']['areas']['tickets']) && !empty($context['search_params']['areas']['replies']))
333
+		{
334
+			// If we're doing both replies and tickets themselves, we don't have to care too much about the message itself, except for being deleted.
335
+			$query = shd_db_query('', '
336
+				SELECT hdssw.id_word, hdt.id_first_msg
337
+				FROM {db_prefix}helpdesk_search_subject_words AS hdssw
338
+					INNER JOIN {db_prefix}helpdesk_tickets AS hdt ON (hdssw.id_ticket = hdt.id_ticket)
339
+				WHERE {query_see_ticket}
340
+					AND id_word IN ({array_string:tokens})' . (empty($_POST['scope_recycle']) || !shd_allowed_to('shd_access_recyclebin', 0) ? '
341
+					AND hdtr.message_status = {int:not_deleted}' : ''),
342
+				array(
343
+					'tokens' => $tokens,
344
+					'not_deleted' => MSG_STATUS_NORMAL,
345
+				)
346
+			);
347
+			while ($row = $smcFunc['db_fetch_assoc']($query))
348
+				$matches['messages'][$row['id_first_msg']][$row['id_word']] = true;
349
+			$smcFunc['db_free_result']($query);
350
+
351
+			if ($context['match_all'])
352
+				foreach ($matches['messages'] as $msg => $ticket_words)
353
+					if (count($ticket_words) != $count_tokens) // How many words did we match in this subject? If it isn't the number we're expecting, ditch it.
354
+						unset($matches['messages'][$msg]);
355
+
356
+			// Now, we just have a list of tickets to play with. Let's put that together in a master list.
357
+			foreach ($matches['messages'] as $msg => $ticket_words)
358
+				$matches['id_msg'][$msg] = true;
359
+			unset($matches['messages']);
360
+		}
361
+		// Just tickets OR replies
362
+		elseif (!empty($context['search_params']['areas']['tickets']) || !empty($context['search_params']['areas']['replies']))
363
+		{
364
+			$query = $smcFunc['db_query']('', '
365
+				SELECT hdstw.id_word, hdstw.id_msg
366
+				FROM {db_prefix}helpdesk_search_ticket_words AS hdstw
367
+					INNER JOIN {db_prefix}helpdesk_ticket_replies AS hdtr ON (hdstw.id_msg = hdtr.id_msg)
368
+					INNER JOIN {db_prefix}helpdesk_tickets AS hdt ON (hdtr.id_ticket = hdt.id_ticket)
369
+				WHERE id_word IN ({array_string:tokens})
370
+					AND hdstw.id_msg {raw:operator} hdt.id_first_msg' . (empty($_POST['scope_recycle']) || !shd_allowed_to('shd_access_recyclebin', 0) ? '
371
+					AND hdtr.message_status = {int:not_deleted}' : ''),
372
+				array(
373
+					'tokens' => $tokens,
374
+					'not_deleted' => MSG_STATUS_NORMAL,
375
+					'operator' => !empty($context['search_params']['areas']['tickets']) ? '=' : '!=',
376
+				)
377
+			);
378
+			while ($row = $smcFunc['db_fetch_assoc']($query))
379
+				$matches['messages'][$row['id_msg']][$row['id_word']] = true;
380
+			$smcFunc['db_free_result']($query);
381
+
382
+			if ($context['match_all'])
383
+				foreach ($matches['messages'] as $ticket => $ticket_words)
384
+					if (count($ticket_words) != $count_tokens) // How many words did we match in this subject? If it isn't the number we're expecting, ditch it.
385
+						unset($matches['messages'][$ticket]);
386
+
387
+			// Now, we just have a list of tickets to play with. Let's put that together in a master list.
388
+			foreach ($matches['messages'] as $msg => $ticket_words)
389
+				$matches['id_msg'][$ticket] = true;
390
+			unset($matches['messages']);
391
+		}
392
+
393
+		// Aw, no matches?
394
+		if (empty($matches['id_msg']))
395
+			return $context['sub_template'] = 'search_no_results';
396
+
397
+		$context['search_clauses'][] = 'hdtr.id_msg IN ({array_int:msg})';
398
+		$context['search_params']['msg'] = array_keys($matches['id_msg']);
399
+
400
+		// How many results are there in total?
401
+		$query = shd_db_query('', '
402
+			SELECT COUNT(*)
403
+			FROM {db_prefix}helpdesk_tickets AS hdt
404
+				INNER JOIN {db_prefix}helpdesk_ticket_replies AS hdtr ON (hdtr.id_ticket = hdt.id_ticket)
405
+			WHERE ' . implode(' AND ', $context['search_clauses']) . ' LIMIT 1000',
406
+			$context['search_params']
407
+		);
408
+		list($count) = $smcFunc['db_fetch_row']($query);
409
+		if ($count == 0)
410
+		{
411
+			$smcFunc['db_free_result']($query);
412
+			return $context['sub_template'] = 'search_no_results';
413
+		}
414
+		// OK, at least one result, awesome. Are we off the end of the list?
415
+		if ($context['search_params']['start'] > $count)
416
+		{
417
+			$context['search_params']['start'] = $count - ($count % $number_per_page);
418
+			$context['pagenum'] = ($context['search_params']['start'] / $number_per_page) + 1;
419
+			$context['num_results'] = $count);
420
+		}
421
+
422
+		// Get the results for displaying.
423
+		$query = shd_db_query('', '
424
+			SELECT hdt.id_ticket, hdt.id_dept, hdd.dept_name, hdt.subject, hdt.urgency, hdt.private, hdt.last_updated, hdtr.body,
425
+				hdtr.smileys_enabled, hdtr.id_member AS id_member, IFNULL(mem.real_name, hdtr.poster_name) AS poster_name, hdtr.poster_time,
426
+				hdt.id_first_msg, hdtr.id_msg
427
+			FROM {db_prefix}helpdesk_ticket_replies AS hdtr
428
+				INNER JOIN {db_prefix}helpdesk_tickets AS hdt ON (hdt.id_ticket = hdtr.id_ticket)
429
+				INNER JOIN {db_prefix}helpdesk_depts AS hdd ON (hdt.id_dept = hdd.id_dept)
430
+				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = hdtr.id_member)
431
+			WHERE ' . implode(' AND ', $context['search_clauses']) . '
432
+			ORDER BY hdt.last_updated DESC, hdtr.id_msg DESC
433
+			LIMIT {int:start}, {int:limit}',
434
+			$context['search_params']
435
+		);
436
+
437
+		$context['search_results'] = array();
438
+		$page_pos = $context['search_params']['start']; // e.g. 0 on page 1, 10 for page 2, the first item will be page_pos + 1, so ++ it before using it.
439
+		while ($row = $smcFunc['db_fetch_assoc']($query))
440
+		{
441
+			$row['result'] = ++$page_pos; // Increment first, then use.
442
+			$row['display_id'] = str_pad($row['id_ticket'], $modSettings['shd_zerofill'], '0', STR_PAD_LEFT);
443
+			$row['is_ticket'] = $row['id_msg'] == $row['id_first_msg']; // If the message we grabbed is the first message, this is actually a ticket, not a reply to one.
444
+			$row['dept_link'] = !$context['shd_multi_dept'] ? '' : '[<a href="' . $scripturl . '?action=helpdesk;sa=main;dept=' . $row['id_dept'] . '">' . $row['dept_name'] . '</a>] ';
445
+
446
+			$context['search_results'][] = $row;
447
+		}
448
+
449
+		return $context['sub_template'] = 'search_results';
450
+	}
451
+}
452
+
453
+function shd_get_named_people($field)
454
+{
455
+	global $smcFunc, $sourcedir, $context;
456
+
457
+	if (!isset($context['named_people']))
458
+		$context['named_people'] = array();
459
+
460
+	require_once($sourcedir . '/Subs-Auth.php');
461
+
462
+	$members = array();
463
+	// First look for the autosuggest values.
464
+	if (!empty($_POST[$field . '_name_from']) && is_array($_POST[$field . '_name_from']))
465
+		foreach ($_POST['starter_name_from'] as $member)
466
+			if ((int) $member > 0)
467
+				$members[] = (int) $member;
468
+
469
+	// Failing that, let's look at the name itself for those without JS.
470
+	if (!empty($_POST[$field . '_name']))
471
+	{
472
+		// We're going to take out the "s anyway ;).
473
+		$names = strtr($_POST[$field . '_name'], array('\\"' => '"'));
474
+
475
+		preg_match_all('~"([^"]+)"~', $names, $matches);
476
+		$namedlist = array_unique(array_merge($matches[1], explode(',', preg_replace('~"[^"]+"~', '', $names))));
477
+
478
+		foreach ($namedlist as $index => $name)
479
+			if (strlen(trim($name)) > 0)
480
+				$namedlist[$index] = $smcFunc['htmlspecialchars']($smcFunc['strtolower'](trim($name)));
481
+			else
482
+				unset($namedlist[$index]);
483
+
484
+		if (!empty($namedlist))
485
+		{
486
+			$foundMembers = findMembers($namedlist);
487
+
488
+			// Assume all are not found, until proven otherwise.
489
+			$namesNotFound[$recipientType] = $namedlist;
490
+
491
+			foreach ($foundMembers as $member)
492
+			{
493
+				$testNames = array(
494
+					$smcFunc['strtolower']($member['username']),
495
+					$smcFunc['strtolower']($member['name']),
496
+					$smcFunc['strtolower']($member['email']),
497
+				);
498
+
499
+				if (count(array_intersect($testNames, $namedRecipientList[$recipientType])) !== 0)
500
+				{
501
+					$members[] = $member['id'];
502
+
503
+					$context['named_people'][$member['id']] = $member['real_name'];
504
+				}
505
+			}
506
+		}
507
+	}
508
+	return array_unique($members);
509
+}
510
+?>
0 511
\ No newline at end of file
... ...
@@ -123,6 +123,8 @@ function shd_main()
123 123
 		'restorereply' => array('SimpleDesk-Delete.php', 'shd_reply_restore'),
124 124
 		'emaillog' => array('SimpleDesk-Notifications.php', 'shd_notify_popup'),
125 125
 		'notify' => array('SimpleDesk-Notifications.php', 'shd_notify_ticket_options'),
126
+		'search' => array('SimpleDesk-Search.php', 'shd_search'),
127
+		'search2' => array('SimpleDesk-Search.php', 'shd_search2'),
126 128
 	);
127 129
 
128 130
 	// Navigation menu
... ...
@@ -162,6 +164,12 @@ function shd_main()
162 164
 			'lang' => true,
163 165
 			'url' => $scripturl . '?action=helpdesk;sa=recyclebin' . $context['shd_dept_link'],
164 166
 		),
167
+		'search' => array(
168
+			'text' => 'shd_search_menu',
169
+			'test' => 'can_shd_search',
170
+			'lang' => true,
171
+			'url' => $scripturl . '?action=helpdesk;sa=search',
172
+		),
165 173
 		// Only for certain sub areas.
166 174
 		'back' => array(
167 175
 			'text' => 'shd_back_to_hd',
... ...
@@ -306,6 +314,7 @@ function shd_main()
306 314
 	$context['can_view_closed'] = shd_allowed_to(array('shd_view_closed_own', 'shd_view_closed_any'), $context['shd_department']);
307 315
 	$context['can_view_recycle'] = shd_allowed_to('shd_access_recyclebin', $context['shd_department']);
308 316
 	$context['display_back_to_hd'] = !in_array($_REQUEST['sa'], array('main', 'viewblock', 'recyclebin', 'closedtickets', 'dept'));
317
+	$context['can_shd_search'] = shd_allowed_to('shd_search', 0);
309 318
 	$context['can_view_options'] = shd_allowed_to(array('shd_view_preferences_own', 'shd_view_preferences_any'), 0);
310 319
 
311 320
 	// Highlight the correct button.
... ...
@@ -469,6 +469,8 @@ function shd_admin_bootstrap(&$admin_areas)
469 469
 					'icon' => 'shd/maintenance.png',
470 470
 					'function' => 'shd_admin_main',
471 471
 					'subsections' => array(
472
+						'main' => array($txt['shd_admin_maint']),
473
+						'search' => array($txt['shd_maint_search_settings']),
472 474
 					),
473 475
 				),
474 476
 			),
... ...
@@ -60,8 +60,9 @@ function shd_load_all_permission_sets()
60 60
 		'admin_helpdesk' => array(false, 'general', ''), // because they'll be managed from parent roles instead
61 61
 		'shd_view_ticket' => array(true, 'general', 'ticket.png'),
62 62
 		'shd_view_ticket_private' => array(true, 'general', 'ticket_private.png'),
63
-		'shd_view_ip' => array(true, 'general', 'ip.png'),
64 63
 		'shd_view_closed' => array(true, 'general', 'log_resolve.png'),
64
+		'shd_view_ip' => array(true, 'general', 'ip.png'),
65
+		'shd_search' => array(false, 'general', 'search.png'),
65 66
 
66 67
 		'shd_new_ticket' => array(false, 'posting', 'log_newticket.png'),
67 68
 		'shd_edit_ticket' => array(true, 'posting', 'log_editticket.png'),
... ...
@@ -164,6 +165,7 @@ function shd_load_role_templates()
164 165
 				'shd_view_ticket_private_any' => ROLEPERM_ALLOW,
165 166
 				'shd_view_closed_any' => ROLEPERM_ALLOW,
166 167
 				'shd_view_ip_own' => ROLEPERM_ALLOW,
168
+				'shd_search' => ROLEPERM_ALLOW,
167 169
 				'shd_new_ticket' => ROLEPERM_ALLOW,
168 170
 				'shd_edit_ticket_any' => ROLEPERM_ALLOW,
169 171
 				'shd_reply_ticket_any' => ROLEPERM_ALLOW,
... ...
@@ -208,6 +210,7 @@ function shd_load_role_templates()
208 210
 				'shd_view_ticket_private_any' => ROLEPERM_ALLOW,
209 211
 				'shd_view_closed_any' => ROLEPERM_ALLOW,
210 212
 				'shd_view_ip_any' => ROLEPERM_ALLOW,
213
+				'shd_search' => ROLEPERM_ALLOW,
211 214
 				'shd_new_ticket' => ROLEPERM_ALLOW,
212 215
 				'shd_edit_ticket_any' => ROLEPERM_ALLOW,
213 216
 				'shd_reply_ticket_any' => ROLEPERM_ALLOW,
... ...
@@ -78,7 +78,7 @@ if (!defined('SMF'))
78 78
  */
79 79
 function shd_create_ticket_post(&$msgOptions, &$ticketOptions, &$posterOptions)
80 80
 {
81
-	global $user_info, $txt, $modSettings, $smcFunc, $context, $user_profile;
81
+	global $user_info, $txt, $modSettings, $smcFunc, $context, $user_profile, $sourcedir;
82 82
 
83 83
 	// Clean them incoming vars up good 'n' proper
84 84
 	$msgOptions['smileys_enabled'] = !empty($msgOptions['smileys_enabled']);
... ...
@@ -365,6 +365,42 @@ function shd_create_ticket_post(&$msgOptions, &$ticketOptions, &$posterOptions)
365 365
 		$smcFunc['db_free_result']($query);
366 366
 	}
367 367
 
368
+	if (empty($context['shd_no_search']))
369
+	{
370
+		// Add words to the tables.
371
+		require_once($sourcedir . '/sd_source/Subs-SimpleDeskSearch.php');
372
+		$words = shd_tokeniser($msgOptions['body']);
373
+		if (!empty($words))
374
+		{
375
+			$rows = array();
376
+			foreach ($words as $word)
377
+				$rows[] = array($word, $msgOptions['id']);
378
+			$smcFunc['db_insert']('replace',
379
+				'{db_prefix}helpdesk_search_ticket_words',
380
+				array('id_word' => 'string', 'id_msg' => 'int'),
381
+				$rows,
382
+				array('id_word', 'id_msg')
383
+			);
384
+		}
385
+
386
+		if ($new_ticket)
387
+		{
388
+			$words = shd_tokeniser($ticketOptions['subject']);
389
+			if (!empty($words))
390
+			{
391
+				$rows = array();
392
+				foreach ($words as $word)
393
+					$rows[] = array($word, $ticketOptions['id']);
394
+				$smcFunc['db_insert']('replace',
395
+					'{db_prefix}helpdesk_search_subject_words',
396
+					array('id_word' => 'string', 'id_ticket' => 'int'),
397
+					$rows,
398
+					array('id_word', 'id_ticket')
399
+				);
400
+			}
401
+		}
402
+	}
403
+
368 404
 	if (!empty($ticketOptions['dept']))
369 405
 		shd_clear_active_tickets($ticketOptions['dept']);
370 406
 
... ...
@@ -416,7 +452,7 @@ function shd_create_ticket_post(&$msgOptions, &$ticketOptions, &$posterOptions)
416 452
 */
417 453
 function shd_modify_ticket_post(&$msgOptions, &$ticketOptions, &$posterOptions)
418 454
 {
419
-	global $user_info, $txt, $modSettings, $smcFunc, $context;
455
+	global $user_info, $txt, $modSettings, $smcFunc, $context, $sourcedir;
420 456
 
421 457
 	$messages_columns = array();
422 458
 	$ticket_columns = array();
... ...
@@ -653,6 +689,60 @@ function shd_modify_ticket_post(&$msgOptions, &$ticketOptions, &$posterOptions)
653 689
 		);
654 690
 	}
655 691
 
692
+	if (empty($context['shd_no_search']) && !empty($msgOptions['id']))
693
+	{
694
+		// Clear the original entries.
695
+		$smcFunc['db_query']('', '
696
+			DELETE FROM {db_prefix}helpdesk_search_ticket_words
697
+			WHERE id_msg = {int:msg}',
698
+			array(
699
+				'msg' => $msgOptions['id'],
700
+			)
701
+		);
702
+		// Add words to the tables.
703
+		require_once($sourcedir . '/sd_source/Subs-SimpleDeskSearch.php');
704
+		if (!empty($msgOptions['body']))
705
+		{
706
+			$words = shd_tokeniser($msgOptions['body']);
707
+			if (!empty($words))
708
+			{
709
+				$rows = array();
710
+				foreach ($words as $word)
711
+					$rows[] = array($word, $msgOptions['id']);
712
+				$smcFunc['db_insert']('replace',
713
+					'{db_prefix}helpdesk_search_ticket_words',
714
+					array('id_word' => 'string', 'id_msg' => 'int'),
715
+					$rows,
716
+					array('id_word', 'id_msg')
717
+				);
718
+			}
719
+		}
720
+
721
+		if (isset($ticketOptions['subject']))
722
+		{
723
+			$smcFunc['db_query']('', '
724
+				DELETE FROM {db_prefix}helpdesk_search_subject_words
725
+				WHERE id_ticket = {int:ticket}',
726
+				array(
727
+					'ticket' => $ticketOptions['id'],
728
+				)
729
+			);
730
+			$words = shd_tokeniser($ticketOptions['subject']);
731
+			if (!empty($words))
732
+			{
733
+				$rows = array();
734
+				foreach ($words as $word)
735
+					$rows[] = array($word, $ticketOptions['id']);
736
+				$smcFunc['db_insert']('replace',
737
+					'{db_prefix}helpdesk_search_subject_words',
738
+					array('id_word' => 'string', 'id_ticket' => 'int'),
739
+					$rows,
740
+					array('id_word', 'id_ticket')
741
+				);
742
+			}
743
+		}
744
+	}
745
+
656 746
 	// Int hook
657 747
 	call_integration_hook('shd_hook_modpost', array(&$msgOptions, &$ticketOptions, &$posterOptions));
658 748
 
... ...
@@ -0,0 +1,140 @@
1
+<?php
2
+###############################################################
3
+#         Simple Desk Project - www.simpledesk.net            #
4
+###############################################################
5
+#       An advanced help desk modifcation built on SMF        #
6
+###############################################################
7
+#                                                             #
8
+#         * Copyright 2010 - SimpleDesk.net                   #
9
+#                                                             #
10
+#   This file and its contents are subject to the license     #
11
+#   included with this distribution, license.txt, which       #
12
+#   states that this software is New BSD Licensed.            #
13
+#   Any questions, please contact SimpleDesk.net              #
14
+#                                                             #
15
+###############################################################
16
+# SimpleDesk Version: 2.0 Anatidae                            #
17
+# File Info: Subs-SimpleDeskSearch.php / 2.0 Anatidae         #
18
+###############################################################
19
+
20
+/**
21
+ *	This file handles the backbone of searches, such as the tokeniser and manages getting the tables actually maintained.
22
+ *
23
+ *	@package source
24
+ *	@since 2.0
25
+*/
26
+
27
+if (!defined('SMF'))
28
+	die('Hacking attempt...');
29
+
30
+/**
31
+ *	Identify and return the character set parameters for searching.
32
+ *
33
+ *	@return An array of two items, the overall character set currently in use and the list of characters to be permitted in searches in the form of a regular expression character class.
34
+ *	@see shd_return_exclude_regex()
35
+*/
36
+function shd_search_charset()
37
+{
38
+	global $context, $modSettings, $txt;
39
+
40
+	$utf8 = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8' && (strpos(strtolower(PHP_OS), 'win') === false || @version_compare(PHP_VERSION, '4.2.3') != -1);
41
+	$charset = !empty($txt['lang_character_set']) ? $txt['lang_character_set'] : ($utf8 ? 'UTF-8' : 'ISO-8859-1');
42
+
43
+	if (empty($modSettings['shd_search_charset']))
44
+		$modSettings['shd_search_charset'] = '0..9, A..Z, a..z, &, ~';
45
+
46
+	$modSettings['shd_search_min_size'] = !empty($modSettings['shd_search_min_size']) ? $modSettings['shd_search_min_size'] : 3;
47
+	$modSettings['shd_search_max_size'] = !empty($modSettings['shd_search_max_size']) ? $modSettings['shd_search_max_size'] : 8;
48
+	$modSettings['shd_search_prefix_size'] = !empty($modSettings['shd_search_prefix_size']) ? $modSettings['shd_search_prefix_size'] : 0;
49
+
50
+	$terms = explode(',', $modSettings['shd_search_charset']);
51
+	$exclude_regex = '';
52
+	foreach ($terms as $k => $v)
53
+	{
54
+		$v = trim($v);
55
+		if (preg_match('~^(.)$~i' . ($context['utf8'] ? 'u' : ''), $v, $match)) // Single character
56
+			$exclude_regex .= preg_quote($match[1], '~');
57
+		elseif (preg_match('~^(.)\.\.(.)$~i' . ($context['utf8'] ? 'u' : ''), $v, $match)) // It's a ranged component.
58
+			$exclude_regex .= preg_quote($match[1], '~') . '-' . preg_quote($match[2], '~');
59
+	}
60
+	if (empty($exclude_regex))
61
+		$exclude_regex = '';
62
+	else
63
+		$exclude_regex = '~[^' . $exclude_regex . ']+~' . ($context['utf8'] ? 'u' : '');
64
+
65
+	return array($charset, $exclude_regex);
66
+}
67
+
68
+/**
69
+ *	Takes an input string and returns a large array of word and word position identifiers.
70
+ *
71
+ *	@param string $string A regular post's contents, or that of the subject of a post.
72
+ *	@return array An array containing the word identifiers.
73
+*/
74
+function shd_tokeniser($string)
75
+{
76
+	global $smcFunc, $modSettings;
77
+	static $charset = null, $exclude_regex = '';
78
+
79
+	$result = array();
80
+
81
+	if ($charset === null)
82
+		list($charset, $exclude_regex) = shd_search_charset();
83
+
84
+	// Step 1. Convert entities back to characters, regardless of what they are.
85
+	$string = html_entity_decode($string, ENT_QUOTES, $charset);
86
+
87
+	// Step 2. Strip wiki code then bbcode.
88
+	$string = preg_replace('~\[\[[^\]]+\]\]~U', '', $string);
89
+	$string = preg_replace('~\[[^\]]+\]~U', '', $string);
90
+
91
+	// Step 3. Strip certain minimal HTML.
92
+	$string = preg_replace('~</?(img|br|hr|b|i|u|strike|s|ins|del|ol|ul|li|p|div|span|table|tr|th|td|code|pre)[^>]+>~iU', ' ', $string);
93
+
94
+	// Step 3. Strip characters we're not interested in.
95
+	if ($exclude_regex === '') // If we have no character types, we can't match anything.
96
+		return array();
97
+
98
+	$string = preg_replace($exclude_regex, ' ', $string);
99
+	$string = trim(preg_replace('~\s+~', ' ', $string));
100
+
101
+	// Step 4. Break into an array and start tokenising.
102
+	$array = explode(' ', $string);
103
+
104
+	$i = 0;
105
+	foreach ($array as $position => $word)
106
+	{
107
+		$len = $smcFunc['strlen']($word);
108
+		if ($len >= $modSettings['shd_search_min_size'] && $len <= $modSettings['shd_search_max_size'])
109
+		{
110
+			$word = $smcFunc['strtolower']($word);
111
+			$result[shd_hash($word)] = $i++;
112
+			if (!empty($modSettings['shd_search_prefix_size']) && $len >= $modSettings['shd_search_prefix_size'])
113
+			{
114
+				for ($j = $modSettings['shd_search_prefix_size']; $j <= $len; $j++)
115
+				{
116
+					$prefixword = substr($word, 0, $j) . chr(7);
117
+					$result[shd_hash($prefixword)] = $i++;
118
+				}
119
+			}
120
+		}
121
+	}
122
+
123
+	return array_flip($result); // This gets us a unique array but done faster than $result[] = shd_hash($word); $result = array_unique($result);
124
+}
125
+
126
+/**
127
+ *	Creates our hash. Due to the way floats can be used, we can safely store an integer equal to 2^52 in a float, so we'll use this. It should be relatively free from avalanching.
128
+ *
129
+ *	Theoretically, a 32 bit hash (a la CRC32) would be suitable if it didn't have the collision incidence factor it does, so we have to do it this way.
130
+ *	If we didn't permit prefix matching it would probably be suitable, actually.
131
+ *
132
+ *	@param string $string The string to take the hash of.
133
+ *	@return string $hash The 52 bit number as a string to prevent it being mashed by any more formatting.
134
+*/
135
+function shd_hash($string)
136
+{
137
+	return sprintf('%0.0f', hexdec(substr(sha1($string), -13)));
138
+}
139
+
140
+?>
0 141
\ No newline at end of file
... ...
@@ -17,20 +17,6 @@ function template_shd_admin_maint_home()
17 17
 {
18 18
 	global $context, $settings, $txt, $modSettings, $scripturl;
19 19
 
20
-	echo '
21
-	<div id="admincenter">
22
-		<div class="tborder">
23
-			<div class="cat_bar">
24
-				<h3 class="catbg">
25
-					<img src="', $settings['default_images_url'], '/simpledesk/maintenance.png" class="icon" alt="*" />
26
-					', $txt['shd_admin_maint'], '
27
-				</h3>
28
-			</div>
29
-			<p class="description">
30
-				', $txt['shd_admin_maint_desc'], '
31
-			</p>
32
-		</div>';
33
-
34 20
 	// OK, recount all the important figures.
35 21
 	echo '
36 22
 		<div class="cat_bar grid_header">
... ...
@@ -189,30 +175,12 @@ function template_shd_admin_maint_home()
189 175
 		</div>
190 176
 		<span class="lowerframe"><span></span></span><br />';
191 177
 	}
192
-
193
-	// And we're done.
194
-	echo '
195
-	</div>';
196 178
 }
197 179
 
198 180
 function template_shd_admin_maint_findrepairdone()
199 181
 {
200 182
 	global $context, $settings, $txt, $scripturl;
201 183
 
202
-	echo '
203
-	<div id="admincenter">
204
-		<div class="tborder">
205
-			<div class="cat_bar">
206
-				<h3 class="catbg">
207
-					<img src="', $settings['default_images_url'], '/simpledesk/find_repair.png" class="icon" alt="*" />
208
-					', $txt['shd_admin_maint_findrepair'], '
209
-				</h3>
210
-			</div>
211
-			<p class="description">
212
-				', $txt['shd_admin_maint_findrepair_desc'], '
213
-			</p>
214
-		</div>';
215
-
216 184
 	if (empty($context['maintenance_result']))
217 185
 	{
218 186
 		// Yay everything was fine.
... ...
@@ -268,10 +236,6 @@ function template_shd_admin_maint_findrepairdone()
268 236
 			<span class="botslice"><span></span></span>
269 237
 		</div>';
270 238
 	}
271
-
272
-	// And we're done.
273
-	echo '
274
-	</div>';
275 239
 }
276 240
 
277 241
 function template_shd_admin_maint_reattributedone()
... ...
@@ -279,18 +243,6 @@ function template_shd_admin_maint_reattributedone()
279 243
 	global $context, $settings, $txt, $scripturl;
280 244
 
281 245
 	echo '
282
-	<div id="admincenter">
283
-		<div class="tborder">
284
-			<div class="cat_bar">
285
-				<h3 class="catbg">
286
-					<img src="', $settings['default_images_url'], '/simpledesk/user.png" class="icon" alt="*" />
287
-					', $txt['shd_admin_maint_reattribute'], '
288
-				</h3>
289
-			</div>
290
-			<p class="description">
291
-				', $txt['shd_admin_maint_reattribute_desc'], '
292
-			</p>
293
-		</div>
294 246
 		<div class="windowbg">
295 247
 			<span class="topslice"><span></span></span>
296 248
 			<div class="content">
... ...
@@ -300,7 +252,6 @@ function template_shd_admin_maint_reattributedone()
300 252
 				</p>
301 253
 			</div>
302 254
 			<span class="botslice"><span></span></span>
303
-		</div>
304 255
 		</div>';
305 256
 }
306 257
 
... ...
@@ -310,17 +261,6 @@ function template_shd_admin_maint_massdeptmovedone()
310 261
 
311 262
 	echo '
312 263
 	<div id="admincenter">
313
-		<div class="tborder">
314
-			<div class="cat_bar">
315
-				<h3 class="catbg">
316
-					<img src="', $settings['default_images_url'], '/simpledesk/movedept.png" class="icon" alt="*" />
317
-					', $txt['shd_admin_maint_massdeptmove'], '
318
-				</h3>
319
-			</div>
320
-			<p class="description">
321
-				', $txt['shd_admin_maint_massdeptmove_desc'], '
322
-			</p>
323
-		</div>
324 264
 		<div class="windowbg">
325 265
 			<span class="topslice"><span></span></span>
326 266
 			<div class="content">
... ...
@@ -334,4 +274,77 @@ function template_shd_admin_maint_massdeptmovedone()
334 274
 	</div>';
335 275
 }
336 276
 
277
+function template_shd_admin_maint_search()
278
+{
279
+	global $context, $settings, $txt, $scripturl, $modSettings;
280
+
281
+	if (isset($_GET['rebuilddone']))
282
+		echo '
283
+		<div class="maintenance_finished">
284
+			', $txt['shd_search_rebuilt'], '
285
+		</div>';
286
+
287
+	echo '
288
+		<div class="cat_bar grid_header">
289
+			<h3 class="catbg">
290
+				<img src="', $settings['default_images_url'], '/simpledesk/search.png" alt="*">
291
+				', $txt['shd_maint_rebuild_index'], '
292
+			</h3>
293
+		</div>
294
+		<div class="roundframe">
295
+			<div class="content">
296
+				<p>', $txt['shd_maint_rebuild_index_desc'], '</p>
297
+				<form action="', $scripturl, '?action=admin;area=helpdesk_maint;sa=search" method="post">
298
+					<input type="submit" name="rebuild" value="', $txt['maintain_run_now'], '" onclick="return submitThisOnce(this);" class="button_submit">
299
+					<input type="hidden" name="start" value="0" />
300
+					<input type="hidden" name="', $context['session_var'], '" value="', $context['session_id'], '">
301
+				</form>
302
+			</div>
303
+		</div>
304
+		<span class="lowerframe"><span></span></span><br />
305
+		<div class="cat_bar grid_header">
306
+			<h3 class="catbg">
307
+				<img src="', $settings['default_images_url'], '/simpledesk/search.png" alt="*">
308
+				', $txt['shd_maint_search_settings'], '
309
+			</h3>
310
+		</div>
311
+		<div class="roundframe">
312
+			<div class="content">
313
+				<form action="', $scripturl, '?action=admin;area=helpdesk_maint;sa=search" method="post">
314
+					<div class="errorbox"><img src="', $settings['default_images_url'], '/simpledesk/warning.png" alt="*" class="shd_icon_minihead" /> &nbsp;', $txt['shd_maint_search_settings_warning'], '</div>
315
+					<dl class="settings">
316
+						<dt>
317
+							', $txt['shd_search_min_size'], '
318
+						</dt>
319
+						<dd>
320
+							<input type="text" class="input_text" name="shd_search_min_size" size="4" value="', $modSettings['shd_search_min_size'], '" />
321
+						</dd>
322
+						<dt>
323
+							', $txt['shd_search_max_size'], '
324
+						</dt>
325
+						<dd>
326
+							<input type="text" class="input_text" name="shd_search_max_size" size="4" value="', $modSettings['shd_search_max_size'], '" />
327
+						</dd>
328
+						<dt>
329
+							<a id="setting_shd_search_prefix_size" href="', $scripturl, '?action=helpadmin;help=shd_search_prefix_size_help" onclick="return reqWin(this.href);" class="help"><img src="', $settings['images_url'], '/helptopics.gif" class="icon" alt="?"></a>
330
+							<span>', $txt['shd_search_prefix_size'], '</span>
331
+						</dt>
332
+						<dd>
333
+							<input type="text" class="input_text" name="shd_search_prefix_size" size="4" value="', $modSettings['shd_search_prefix_size'], '" />
334
+						</dd>
335
+						<dt>
336
+							', $txt['shd_search_charset'], '
337
+						</dt>
338
+						<dd>
339
+							<textarea name="shd_search_charset" rows="3" cols="35" style="width: 99%;">', htmlspecialchars($modSettings['shd_search_charset']), '</textarea>
340
+						</dd>
341
+					</dl>
342
+					<span><input type="submit" name="save" value="', $txt['save'], '" class="button_submit" /></span>
343
+					<input type="hidden" name="', $context['session_var'], '" value="', $context['session_id'], '">
344
+				</form>
345
+			</div>
346
+		</div>
347
+		<span class="lowerframe"><span></span></span><br />';
348
+}
349
+
337 350
 ?>
338 351
\ No newline at end of file
... ...
@@ -0,0 +1,357 @@
1
+<?php
2
+// Version: 2.0 Anatidae; SimpleDesk searches.
3
+
4
+/**
5
+ *	Handles searching tickets.
6
+ *
7
+ *	@package template
8
+ *	@since 1.0
9
+*/
10
+
11
+/**
12
+ *	Displays the list of possible searching criteria.
13
+ *
14
+ *	@see shd_search()
15
+ *	@since 2.0
16
+*/
17
+function template_search()
18
+{
19
+	global $context, $txt, $scripturl, $settings, $modSettings;
20
+
21
+	// Back to the helpdesk.
22
+	echo '
23
+		<div class="floatleft">
24
+			', template_button_strip(array($context['navigation']['back']), 'bottom'), '
25
+		</div><br class="clear" /><br />';
26
+
27
+	if (!empty($modSettings['shd_new_search_index']))
28
+		echo '
29
+	<div class="errorbox"><img src="', $settings['default_images_url'], '/simpledesk/warning.png" alt="*" class="shd_icon_minihead" /> &nbsp;', shd_allowed_to('admin_helpdesk', 0) ? $txt['shd_search_warning_admin'] : $txt['shd_search_warning_nonadmin'], '</div>';
30
+
31
+	echo '
32
+	<div class="cat_bar grid_header">
33
+		<h3 class="catbg">
34
+			<img src="', $settings['default_images_url'], '/simpledesk/search.png" alt="*" />
35
+			', $txt['shd_search'], '
36
+		</h3>
37
+	</div>
38
+	<div class="roundframe">
39
+		<form action="', $scripturl, '?action=helpdesk;sa=search2" method="post">
40
+			<div class="content">
41
+				<br />
42
+				<dl class="settings">
43
+					<dt>
44
+						<strong>', $txt['shd_search_text'], '</strong>
45
+					</dt>
46
+					<dd>
47
+						<input type="text" name="search" value="" size="40" maxlength="100" class="input_text" />
48
+					</dd>
49
+					<dt>
50
+						<strong>', $txt['shd_search_match'], '</strong>
51
+					</dt>
52
+					<dd>
53
+						<select name="searchtype">
54
+							<option value="all">', $txt['shd_search_match_all'], '</option>
55
+							<option value="any">', $txt['shd_search_match_any'], '</option>
56
+						</select>
57
+					</dd>
58
+				</dl>
59
+				<br />
60
+				<dl class="settings">
61
+					<dt>
62
+						<strong>', $txt['shd_search_where'], '</strong>
63
+					</dt>
64
+					<dd>
65
+						<input type="checkbox" class="input_check" checked="checked" name="search_subjects" /> ', $txt['shd_search_where_subjects'], '<br />
66
+						<input type="checkbox" class="input_check" checked="checked" name="search_tickets" /> ', $txt['shd_search_where_tickets'], '<br />
67
+						<input type="checkbox" class="input_check" checked="checked" name="search_replies" /> ', $txt['shd_search_where_replies'], '<br />
68
+					</dd>
69
+				</dl>';
70
+
71
+	if (count($context['dept_list']) == 1)
72
+	{
73
+		$array = array_keys($context['dept_list']);
74
+		echo '
75
+					<input type="hidden" name="search_dept[]" value="', $array[0], '" />';
76
+	}
77
+	else
78
+	{
79
+		echo '
80
+				<hr />
81
+				<dl class="settings">
82
+					<dt>
83
+						<strong>', $txt['shd_search_dept'], '</strong>
84
+					</dt>
85
+					<dd>';
86
+
87
+		foreach ($context['dept_list'] as $id => $name)
88
+			echo '
89
+							<input type="checkbox" class="input_check" checked="checked" name="search_dept[]" value="', $id, '" /> &nbsp;', $name, '<br />';
90
+
91
+		echo '
92
+					</dd>
93
+				</dl>';
94
+	}
95
+
96
+	echo '
97
+				<hr />
98
+				<dl class="settings">
99
+					<dt>
100
+						<strong>', $txt['shd_search_scope'], '</strong>
101
+					</dt>
102
+					<dd>
103
+						<input type="checkbox" class="input_check" checked="checked" name="scope_open" /> &nbsp;', $txt['shd_search_scope_open'], '<br />
104
+						<input type="checkbox" class="input_check" checked="checked" name="scope_closed" /> &nbsp;', $txt['shd_search_scope_closed'], '<br />
105
+						<input type="checkbox" class="input_check" checked="checked" name="scope_recycle" /> &nbsp;', $txt['shd_search_scope_recycle'], '<br />
106
+					</dd>
107
+				</dl>
108
+				<hr />
109
+				<dl class="settings">
110
+					<dt>
111
+						<strong>', $txt['shd_search_urgency'], '</strong>
112
+					</dt>
113
+					<dd>';
114
+
115
+	// All the urgency levels, currently 0-5.
116
+	for ($i = 0; $i <= 5; $i++)
117
+		echo '
118
+						<input type="checkbox" class="input_check" checked="checked" name="urgency[]" value="', $i, '" /> &nbsp;', $txt['shd_urgency_' . $i], '<br />';
119
+
120
+	echo '
121
+					</dd>
122
+				</dl>
123
+				<hr />
124
+				<dl class="settings">
125
+					<dt>
126
+						<strong>', $txt['shd_search_ticket_starter'], '</strong>
127
+						<div class="smalltext">', $txt['shd_search_ticket_named_person'], '</div>
128
+					</dt>
129
+					<dd>
130
+						<input type="hidden" name="starter" value="" />
131
+						<input type="text" name="starter_name" id="starter_name" size="40" maxlength="100" class="input_text" value="" />
132
+						<div id="starter_name_container"></div>
133
+					</dd>
134
+				</dl>
135
+				<br />
136
+				<dl class="settings">
137
+					<dt>
138
+						<strong>', $txt['shd_search_ticket_assignee'], '</strong>
139
+						<div class="smalltext">', $txt['shd_search_ticket_named_person'], '</div>
140
+					</dt>
141
+					<dd>
142
+						<input type="hidden" name="assignee" value="" />
143
+						<input type="text" name="assignee_name" id="assignee_name" size="40" maxlength="100" class="input_text" value="" />
144
+						<div id="assignee_name_container"></div>
145
+					</dd>
146
+				</dl>
147
+
148
+				<script type="text/javascript" src="', $settings['default_theme_url'], '/scripts/suggest.js?20fin"></script>
149
+				<script type="text/javascript"><!-- // --><![CDATA[
150
+					var oTicketStarter = new smc_AutoSuggest({
151
+						sSelf: \'oTicketStarter\',
152
+						sSessionId: \'', $context['session_id'], '\',
153
+						sSessionVar: \'', $context['session_var'], '\',
154
+						sControlId: \'starter_name\',
155
+						sSuggestId: \'starter\',
156
+						sSearchType: \'member\',
157
+						sPostName: \'starter_name_form\',
158
+						sURLMask: \'action=profile;u=%item_id%\',
159
+						bItemList: true,
160
+						sItemListContainerId: \'starter_name_container\',
161
+						aListItems: []
162
+					});
163
+					var oTicketAssignee = new smc_AutoSuggest({
164
+						sSelf: \'oTicketAssignee\',
165
+						sSessionId: \'', $context['session_id'], '\',
166
+						sSessionVar: \'', $context['session_var'], '\',
167
+						sControlId: \'assignee_name\',
168
+						sSuggestId: \'assignee\',
169
+						sSearchType: \'member\',
170
+						sPostName: \'assigned_name_form\',
171
+						sURLMask: \'action=profile;u=%item_id%\',
172
+						bItemList: true,
173
+						sItemListContainerId: \'assignee_name_container\',
174
+						aListItems: []
175
+					});
176
+				// ]', ']></script>
177
+				<hr />
178
+				<br />
179
+				<input type="submit" value="', $txt['shd_search'], '" onclick="return submitThisOnce(this);" accesskey="s" class="button_submit" />
180
+			</div>
181
+		</form>
182
+	</div>
183
+	<span class="lowerframe"><span></span></span>';
184
+}
185
+
186
+function template_search_no_results()
187
+{
188
+	global $context, $txt, $scripturl, $settings, $modSettings;
189
+
190
+	// Back to the helpdesk.
191
+	echo '
192
+		<div class="floatleft">
193
+			', template_button_strip(array($context['navigation']['back'], $context['navigation']['search']), 'bottom'), '
194
+		</div><br class="clear" /><br />';
195
+
196
+	echo '
197
+	<div class="cat_bar grid_header">
198
+		<h3 class="catbg">
199
+			<img src="', $settings['default_images_url'], '/simpledesk/search.png" alt="*" />
200
+			', $txt['shd_search'], '
201
+		</h3>
202
+	</div>';
203
+
204
+	// Search criteria
205
+	template_search_criteria();
206
+
207
+	echo '
208
+	<span class="upperframe"><span></span></span>
209
+	<div class="roundframe">
210
+		<div class="content">', $txt['shd_search_no_results'], '</div>
211
+	</div>
212
+	<span class="lowerframe"><span></span></span>';
213
+}
214
+
215
+function template_search_criteria()
216
+{
217
+	global $context, $txt, $scripturl, $settings, $modSettings, $smcFunc;
218
+
219
+	if (!empty($context['search_params']))
220
+	{
221
+		echo '
222
+	<div class="information">
223
+		<strong>', $txt['shd_search_criteria'], '</strong>
224
+		<ul>';
225
+
226
+		// We go through the form step by step.
227
+		if (!empty($context['search_terms']))
228
+		{
229
+			echo '
230
+			<li>', $txt['shd_search_text'], ' ', $smcFunc['htmlspecialchars']($context['search_terms']), ' (', $context['match_all'] ? $txt['shd_search_match_all'] : $txt['shd_search_match_any'], ')</li>';
231
+
232
+			// Since we're here, we also need to attend to which items we searched.
233
+			$items = array();
234
+			foreach ($context['search_params']['areas'] as $k => $v)
235
+				$items[] = $txt['shd_search_where_' . $k];
236
+
237
+			echo '
238
+			<li>', $txt['shd_search_where'], ' ', implode(', ', $items), '</li>';
239
+		}
240
+
241
+		// Departments. Don't bother if the user can only see one department.
242
+		if (!empty($context['search_dept_list']))
243
+			echo '
244
+			<li>', $txt['shd_search_dept'], ' ', implode(', ', $context['search_dept_list']), '</li>';
245
+
246
+		// What type of tickets?
247
+		if (!empty($context['search_params']['status']))
248
+		{
249
+			$status = array();
250
+			if (!empty($_POST['scope_open']))
251
+				$status[] = $txt['shd_search_scope_open'];
252
+			if (!empty($_POST['scope_closed']))
253
+				$status[] = $txt['shd_search_scope_closed'];
254
+			if (!empty($_POST['scope_recycle']))
255
+				$status[] = $txt['shd_search_scope_recycle'];
256
+
257
+			echo '
258
+			<li>', $txt['shd_search_scope'], ' ', implode(', ', $status), '</li>';
259
+		}
260
+
261
+		// Ticket urgency
262
+		if (!empty($context['search_params']['urgency']))
263
+		{
264
+			$urgency = $context['search_params']['urgency'];
265
+			sort($urgency);
266
+			foreach ($urgency as $k => $v)
267
+				$urgency[$k] = $txt['shd_urgency_' . $v];
268
+
269
+			echo '
270
+			<li>', $txt['shd_search_urgency'], ' ', implode(', ', $urgency), '</li>';
271
+		}
272
+
273
+		// Tickets started by
274
+		if (!empty($context['search_params']['member_started']))
275
+		{
276
+			$members = $context['search_params']['member_started'];
277
+			// This is a list of ids we pulled via findMember(). We should have their names having found their ids.
278
+			foreach ($members as $k => $v)
279
+				$members[$k] = shd_profile_link($context['named_people'][$v], $v);
280
+
281
+			echo '
282
+			<li>', $txt['shd_search_ticket_starter'], ' ', implode(', ', $members), '</li>';
283
+		}
284
+
285
+		// Tickets assigned
286
+		if (!empty($context['search_params']['member_assigned']))
287
+		{
288
+			$members = $context['search_params']['member_assigned'];
289
+			// This is a list of ids we pulled via findMember(). We should have their names having found their ids.
290
+			foreach ($members as $k => $v)
291
+				$members[$k] = shd_profile_link($context['named_people'][$v], $v);
292
+
293
+			echo '
294
+			<li>', $txt['shd_search_ticket_assignee'], ' ', implode(', ', $members), '</li>';
295
+		}
296
+
297
+		echo '
298
+		</ul>
299
+		<em>', $txt['shd_search_excluded'], '</em>
300
+	</div>';
301
+	}
302
+}
303
+
304
+function template_search_results()
305
+{
306
+	global $context, $txt, $scripturl, $settings, $modSettings, $smcFunc;
307
+
308
+	// Back to the helpdesk.
309
+	echo '
310
+		<div class="floatleft">
311
+			', template_button_strip(array($context['navigation']['back'], $context['navigation']['search']), 'bottom'), '
312
+		</div><br class="clear" /><br />';
313
+
314
+	echo '
315
+	<div class="cat_bar">
316
+		<h3 class="catbg">
317
+			<img src="', $settings['default_images_url'], '/simpledesk/search.png" alt="*" />
318
+			', $txt['shd_search_results'], '
319
+		</h3>
320
+	</div>';
321
+
322
+	// Page navigation. It's not your usual page index, and with good reason: we can't use regular links here without risking server hammering.
323
+	$num_pages = ceil($context['num_results'] / $context['search_params']['limit']);
324
+	$pages = array();
325
+	foreach ($page = $context['numpage'] - 2; $page <= $context['numpage'] + 2; $page++)
326
+		$pages[] = $page;
327
+
328
+	// The rest of it would go here, in a nice form that carried everything through for next time, with a button named page whose value would be the page number for each page (plus prev/next) you wanted to display
329
+
330
+	// Search criteria
331
+	template_search_criteria();
332
+
333
+	// And finally, the results themselves.
334
+	$use_bg2 = false;
335
+
336
+	foreach ($context['search_results'] as $index => $result)
337
+	{
338
+				echo '
339
+	<div class="search_results_posts">
340
+		<div class="windowbg', $use_bg2 ? '2' : '', ' core_posts">
341
+			<span class="topslice"><span></span></span>
342
+			<div class="content flow_auto">
343
+				<div class="topic_details floatleft" style="width: 94%">
344
+					<div class="counter">', $result['result'], '</div>
345
+					<h5>', $result['dept_link'], '<a href="', $scripturl, '?action=helpdesk;sa=ticket;ticket=', $result['id_ticket'], '">', sprintf($result['is_ticket'] ? $txt['shd_search_result_ticket'] : $txt['shd_search_result_reply'], $result['display_id']), '</a> - ', $result['subject'], ' (', $txt['shd_search_last_updated'], ' ', timeformat($result['last_updated']), ')</h5>
346
+					<span class="smalltext">&#171;&nbsp;<strong>', $result['is_ticket'] ? $txt['shd_search_ticket_opened_by'] : $txt['shd_search_ticket_replied_by'], ' ', shd_profile_link($result['poster_name'], $result['id_member']), '</strong>&nbsp;', $txt['on'], '&nbsp;<em>', timeformat($result['poster_time']), '</em>&nbsp;&#187;</span>
347
+				</div>
348
+				<br class="clear">
349
+				<div class="list_posts double_height">', $result['body'], '</div>
350
+			</div>
351
+			<span class="botslice"><span></span></span>
352
+		</div>
353
+	</div>';
354
+				$use_bg2 = !$use_bg2;
355
+	}
356
+}
357
+?>
0 358
\ No newline at end of file
1 359