1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8class Search_MySql_Table extends TikiDb_Table
9{
10	private $definition = false;
11	private $indexes = [];
12	private $exists = null;
13
14	private $schemaBuffer;
15	private $dataBuffer;
16	private $tfTranslator;
17
18	function __construct($db, $table)
19	{
20		parent::__construct($db, $table);
21
22		$table = $this->escapeIdentifier($this->tableName);
23		$this->schemaBuffer = new Search_MySql_QueryBuffer($db, 2000, "ALTER TABLE $table ");
24		$this->dataBuffer = new Search_MySql_QueryBuffer($db, 100, '-- '); // Null Object, replaced later
25		$this->tfTranslator = new Search_MySql_TrackerFieldTranslator;
26	}
27
28	function __destruct()
29	{
30		try {
31			$this->flush();
32		} catch (Search_MySql_Exception $e) {
33			# ignore this to cleanly destruct the object
34		}
35	}
36
37	function drop()
38	{
39		$table = $this->escapeIdentifier($this->tableName);
40		$this->db->query("DROP TABLE IF EXISTS $table");
41		$this->definition = false;
42		$this->exists = false;
43
44		$this->emptyBuffer();
45	}
46
47	function exists()
48	{
49		if (is_null($this->exists)) {
50			$tables = $this->db->listTables();
51			$this->exists = in_array($this->tableName, $tables);
52		}
53
54		return $this->exists;
55	}
56
57	function insert(array $values, $ignore = false)
58	{
59		$keySet = implode(', ', array_map([$this, 'escapeIdentifier'], array_map([$this->tfTranslator, 'shortenize'], array_keys($values))));
60
61		$valueSet = '(' . implode(', ', array_map([$this->db, 'qstr'], $values)) . ')';
62
63		$this->addToBuffer($keySet, $valueSet);
64
65		return 0;
66	}
67
68	function ensureHasField($fieldName, $type)
69	{
70		$this->loadDefinition();
71
72		if (! isset($this->definition[$fieldName])) {
73			$this->addField($fieldName, $type);
74			$this->definition[$fieldName] = $type;
75		}
76	}
77
78	function hasIndex($fieldName, $type)
79	{
80		$this->loadDefinition();
81
82		$indexName = $fieldName . '_' . $type;
83		return isset($this->indexes[$indexName]);
84	}
85
86	function ensureHasIndex($fieldName, $type)
87	{
88		global $prefs;
89
90		$this->loadDefinition();
91
92		if (! isset($this->definition[$fieldName]) && $prefs['search_error_missing_field'] === 'y') {
93			if (preg_match('/^tracker_field_/', $fieldName)) {
94				$msg = tr('Field %0 does not exist in the current index. Please check field permanent name and if you have any items in that tracker.', $fieldName);
95				if ($prefs['unified_exclude_nonsearchable_fields'] === 'y') {
96					$msg .= ' '.tr('You have disabled indexing non-searchable tracker fields. Check if this field is marked as searchable.');
97				}
98			} else {
99				$msg = tr('Field %0 does not exist in the current index. If this is a tracker field, the proper syntax is tracker_field_%0.', $fieldName, $fieldName);
100			}
101			$e = new Search_MySql_QueryException($msg);
102			if ($fieldName == 'tracker_id') {
103				$e->suppress_feedback = true;
104			}
105			throw $e;
106		}
107
108		$indexName = $fieldName . '_' . $type;
109
110		// Static MySQL limit on 64 indexes per table
111		if (! isset($this->indexes[$indexName]) && count($this->indexes) < 64) {
112			if ($type == 'fulltext') {
113				$this->addFullText($fieldName);
114			} elseif ($type == 'index') {
115				$this->addIndex($fieldName);
116			}
117
118			$this->indexes[$indexName] = true;
119		}
120	}
121
122	private function loadDefinition()
123	{
124		if (! empty($this->definition)) {
125			return;
126		}
127
128		if (! $this->exists()) {
129			$this->createTable();
130			$this->loadDefinition();
131		}
132
133		$table = $this->escapeIdentifier($this->tableName);
134		$result = $this->db->fetchAll("DESC $table");
135		$this->definition = [];
136		foreach ($result as $row) {
137			$this->definition[$this->tfTranslator->normalize($row['Field'])] = $row['Type'];
138		}
139
140		$result = $this->db->fetchAll("SHOW INDEXES FROM $table");
141		$this->indexes = [];
142		foreach ($result as $row) {
143			$this->indexes[$this->tfTranslator->normalize($row['Key_name'])] = true;
144		}
145	}
146
147	private function createTable()
148	{
149		$table = $this->escapeIdentifier($this->tableName);
150		$this->db->query(
151			"CREATE TABLE IF NOT EXISTS $table (
152				`id` INT NOT NULL AUTO_INCREMENT,
153				`object_type` VARCHAR(15) NOT NULL,
154				`object_id` VARCHAR(235) NOT NULL,
155				PRIMARY KEY(`id`),
156				INDEX (`object_type`, `object_id`(160))
157			) ENGINE=MyISAM"
158		);
159		$this->exists = true;
160
161		$this->emptyBuffer();
162	}
163
164	private function addField($fieldName, $type)
165	{
166		$table = $this->escapeIdentifier($this->tableName);
167		$fieldName = $this->escapeIdentifier($this->tfTranslator->shortenize($fieldName));
168		$this->schemaBuffer->push("ADD COLUMN $fieldName $type");
169	}
170
171	private function addIndex($fieldName)
172	{
173		$currentType = $this->definition[$fieldName];
174		$alterType = null;
175
176		$indexName = $fieldName . '_index';
177		$table = $this->escapeIdentifier($this->tableName);
178		$escapedIndex = $this->escapeIdentifier($this->tfTranslator->shortenize($indexName));
179		$escapedField = $this->escapeIdentifier($this->tfTranslator->shortenize($fieldName));
180
181		if ($currentType == 'TEXT' || $currentType == 'text') {
182			$this->schemaBuffer->push("MODIFY COLUMN $escapedField VARCHAR(235)");
183			$this->definition[$fieldName] = 'VARCHAR(235)';
184		}
185
186		$this->schemaBuffer->push("ADD INDEX $escapedIndex ($escapedField)");
187	}
188
189	private function addFullText($fieldName)
190	{
191		$indexName = $fieldName . '_fulltext';
192		$table = $this->escapeIdentifier($this->tableName);
193		$escapedIndex = $this->escapeIdentifier($this->tfTranslator->shortenize($indexName));
194		$escapedField = $this->escapeIdentifier($this->tfTranslator->shortenize($fieldName));
195		$this->schemaBuffer->push("ADD FULLTEXT INDEX $escapedIndex ($escapedField)");
196	}
197
198	private function emptyBuffer()
199	{
200		$this->schemaBuffer->clear();
201		$this->dataBuffer->clear();
202	}
203
204	private function addToBuffer($keySet, $valueSet)
205	{
206		$this->schemaBuffer->flush();
207
208		$this->dataBuffer->setPrefix("INSERT INTO {$this->escapeIdentifier($this->tableName)} ($keySet) VALUES ");
209		$this->dataBuffer->push($valueSet);
210	}
211
212	function flush()
213	{
214		$this->schemaBuffer->flush();
215		$this->dataBuffer->flush();
216	}
217}
218