Commit | Line | Data |
---|---|---|
4ed867e0 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
41498ac5 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
4ed867e0 | 5 | | | |
41498ac5 TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
4ed867e0 TO |
9 | +--------------------------------------------------------------------+ |
10 | */ | |
11 | ||
12 | namespace Civi\Core; | |
13 | ||
14 | /** | |
15 | * Class SqlTriggers | |
16 | * @package Civi\Core | |
17 | * | |
18 | * This class manages creation and destruction of SQL triggers. | |
19 | */ | |
20 | class SqlTriggers { | |
21 | ||
9307ddf7 TO |
22 | /** |
23 | * The name of the output file. | |
24 | * | |
cc101011 | 25 | * @var string|null |
9307ddf7 | 26 | */ |
0a089620 EM |
27 | private $file; |
28 | ||
29 | /** | |
30 | * Queries written to file when the class is destructed. | |
31 | * | |
32 | * @var array | |
33 | */ | |
34 | private $enqueuedQueries = []; | |
9307ddf7 | 35 | |
4ed867e0 TO |
36 | /** |
37 | * Build a list of triggers via hook and add them to (err, reconcile them | |
38 | * with) the database. | |
39 | * | |
40 | * @param string $tableName | |
41 | * the specific table requiring a rebuild; or NULL to rebuild all tables. | |
42 | * @param bool $force | |
43 | * | |
44 | * @see CRM-9716 | |
45 | */ | |
46 | public function rebuild($tableName = NULL, $force = FALSE) { | |
c64f69d9 | 47 | $info = []; |
4ed867e0 TO |
48 | |
49 | $logging = new \CRM_Logging_Schema(); | |
50 | $logging->triggerInfo($info, $tableName, $force); | |
51 | ||
52 | \CRM_Core_I18n_Schema::triggerInfo($info, $tableName); | |
53 | \CRM_Contact_BAO_Contact::triggerInfo($info, $tableName); | |
54 | ||
55 | \CRM_Utils_Hook::triggerInfo($info, $tableName); | |
56 | ||
57 | // drop all existing triggers on all tables | |
58 | $logging->dropTriggers($tableName); | |
59 | ||
60 | // now create the set of new triggers | |
61 | $this->createTriggers($info, $tableName); | |
0a089620 | 62 | $this->writeEnqueuedQueriesToFile(); |
4ed867e0 TO |
63 | } |
64 | ||
65 | /** | |
66 | * @param array $info | |
67 | * per hook_civicrm_triggerInfo. | |
68 | * @param string $onlyTableName | |
69 | * the specific table requiring a rebuild; or NULL to rebuild all tables. | |
70 | */ | |
0894c6bc | 71 | public function createTriggers($info, $onlyTableName = NULL) { |
4ed867e0 TO |
72 | // Validate info array, should probably raise errors? |
73 | if (is_array($info) == FALSE) { | |
74 | return; | |
75 | } | |
76 | ||
c64f69d9 | 77 | $triggers = []; |
4ed867e0 TO |
78 | |
79 | // now enumerate the tables and the events and collect the same set in a different format | |
80 | foreach ($info as $value) { | |
81 | ||
82 | // clean the incoming data, skip malformed entries | |
83 | // TODO: malformed entries should raise errors or get logged. | |
84 | if (isset($value['table']) == FALSE || | |
85 | isset($value['event']) == FALSE || | |
86 | isset($value['when']) == FALSE || | |
87 | isset($value['sql']) == FALSE | |
88 | ) { | |
89 | continue; | |
90 | } | |
91 | ||
92 | if (is_string($value['table']) == TRUE) { | |
c64f69d9 | 93 | $tables = [$value['table']]; |
4ed867e0 TO |
94 | } |
95 | else { | |
96 | $tables = $value['table']; | |
97 | } | |
98 | ||
99 | if (is_string($value['event']) == TRUE) { | |
c64f69d9 | 100 | $events = [strtolower($value['event'])]; |
4ed867e0 TO |
101 | } |
102 | else { | |
103 | $events = array_map('strtolower', $value['event']); | |
104 | } | |
105 | ||
106 | $whenName = strtolower($value['when']); | |
107 | ||
108 | foreach ($tables as $tableName) { | |
109 | if (!isset($triggers[$tableName])) { | |
c64f69d9 | 110 | $triggers[$tableName] = []; |
4ed867e0 TO |
111 | } |
112 | ||
113 | foreach ($events as $eventName) { | |
c64f69d9 CW |
114 | $template_params = ['{tableName}', '{eventName}']; |
115 | $template_values = [$tableName, $eventName]; | |
4ed867e0 TO |
116 | |
117 | $sql = str_replace($template_params, | |
118 | $template_values, | |
119 | $value['sql'] | |
120 | ); | |
121 | $variables = str_replace($template_params, | |
122 | $template_values, | |
123 | \CRM_Utils_Array::value('variables', $value) | |
124 | ); | |
125 | ||
126 | if (!isset($triggers[$tableName][$eventName])) { | |
c64f69d9 | 127 | $triggers[$tableName][$eventName] = []; |
4ed867e0 TO |
128 | } |
129 | ||
130 | if (!isset($triggers[$tableName][$eventName][$whenName])) { | |
131 | // We're leaving out cursors, conditions, and handlers for now | |
132 | // they are kind of dangerous in this context anyway | |
133 | // better off putting them in stored procedures | |
c64f69d9 CW |
134 | $triggers[$tableName][$eventName][$whenName] = [ |
135 | 'variables' => [], | |
136 | 'sql' => [], | |
137 | ]; | |
4ed867e0 TO |
138 | } |
139 | ||
140 | if ($variables) { | |
141 | $triggers[$tableName][$eventName][$whenName]['variables'][] = $variables; | |
142 | } | |
143 | ||
144 | $triggers[$tableName][$eventName][$whenName]['sql'][] = $sql; | |
145 | } | |
146 | } | |
147 | } | |
148 | ||
9b0ff7e2 EM |
149 | // Sort tables alphabetically in order to output in a consistent order |
150 | // for sites that like to diff this output over time | |
151 | // (ie. with the logging_no_trigger_permission setting in place). | |
0a089620 | 152 | ksort($triggers); |
4ed867e0 TO |
153 | // now spit out the sql |
154 | foreach ($triggers as $tableName => $tables) { | |
155 | if ($onlyTableName != NULL && $onlyTableName != $tableName) { | |
156 | continue; | |
157 | } | |
158 | foreach ($tables as $eventName => $events) { | |
159 | foreach ($events as $whenName => $parts) { | |
160 | $varString = implode("\n", $parts['variables']); | |
161 | $sqlString = implode("\n", $parts['sql']); | |
162 | $validName = \CRM_Core_DAO::shortenSQLName($tableName, 48, TRUE); | |
163 | $triggerName = "{$validName}_{$whenName}_{$eventName}"; | |
164 | $triggerSQL = "CREATE TRIGGER $triggerName $whenName $eventName ON $tableName FOR EACH ROW BEGIN $varString $sqlString END"; | |
165 | ||
7a29e455 TO |
166 | $this->enqueueQuery("DROP TRIGGER IF EXISTS $triggerName"); |
167 | $this->enqueueQuery($triggerSQL); | |
4ed867e0 TO |
168 | } |
169 | } | |
170 | } | |
171 | } | |
172 | ||
173 | /** | |
174 | * Wrapper function to drop triggers. | |
175 | * | |
176 | * @param string $tableName | |
177 | * the specific table requiring a rebuild; or NULL to rebuild all tables. | |
178 | */ | |
179 | public function dropTriggers($tableName = NULL) { | |
c64f69d9 | 180 | $info = []; |
4ed867e0 TO |
181 | |
182 | $logging = new \CRM_Logging_Schema(); | |
183 | $logging->triggerInfo($info, $tableName); | |
184 | ||
185 | // drop all existing triggers on all tables | |
186 | $logging->dropTriggers($tableName); | |
187 | } | |
188 | ||
9307ddf7 TO |
189 | /** |
190 | * Enqueue a query which alters triggers. | |
191 | * | |
192 | * As this requires a high permission level we funnel the queries through here to | |
193 | * facilitate them being taken 'offline'. | |
194 | * | |
195 | * @param string $triggerSQL | |
196 | * The sql to run to create or drop the triggers. | |
197 | * @param array $params | |
198 | * Optional parameters to interpolate into the string. | |
199 | */ | |
c64f69d9 | 200 | public function enqueueQuery($triggerSQL, $params = []) { |
9307ddf7 TO |
201 | if (\Civi::settings()->get('logging_no_trigger_permission')) { |
202 | ||
203 | if (!file_exists($this->getFile())) { | |
204 | // Ugh. Need to let user know somehow. This is the first change. | |
c64f69d9 | 205 | \CRM_Core_Session::setStatus(ts('The mysql commands you need to run are stored in %1', [ |
34f3bbd9 SL |
206 | 1 => $this->getFile(), |
207 | ]), | |
9307ddf7 TO |
208 | '', |
209 | 'alert', | |
c64f69d9 | 210 | ['expires' => 0] |
9307ddf7 TO |
211 | ); |
212 | } | |
0a089620 EM |
213 | $query = \CRM_Core_DAO::composeQuery($triggerSQL, $params); |
214 | $this->enqueuedQueries[$query] = $query; | |
9307ddf7 TO |
215 | } |
216 | else { | |
217 | \CRM_Core_DAO::executeQuery($triggerSQL, $params, TRUE, NULL, FALSE, FALSE); | |
218 | } | |
219 | } | |
220 | ||
221 | /** | |
222 | * @return NULL|string | |
223 | */ | |
224 | public function getFile() { | |
225 | if ($this->file === NULL) { | |
226 | $prefix = 'trigger' . \CRM_Utils_Request::id(); | |
227 | $config = \CRM_Core_Config::singleton(); | |
228 | $this->file = "{$config->configAndLogDir}CiviCRM." . $prefix . md5($config->dsn) . '.sql'; | |
229 | } | |
230 | return $this->file; | |
231 | } | |
232 | ||
0a089620 EM |
233 | /** |
234 | * Write queries to file when the class is destructed. | |
235 | * | |
236 | * Note this is already written out for a full rebuild but it is | |
237 | * possible (at least in terms of what is public) to call drop & create | |
238 | * separately so this ensures they are output. | |
239 | */ | |
240 | public function __destruct() { | |
241 | $this->writeEnqueuedQueriesToFile(); | |
242 | } | |
243 | ||
244 | /** | |
245 | * Write queries queued for write-to-file. | |
246 | */ | |
247 | protected function writeEnqueuedQueriesToFile(): void { | |
248 | if (!empty($this->enqueuedQueries) && $this->getFile()) { | |
249 | $buf = "DELIMITER //\n"; | |
250 | foreach ($this->enqueuedQueries as $query) { | |
251 | if (strpos($query, 'CREATE TRIGGER') === 0) { | |
252 | // The create triggers are long so put spaces between them. For the drops | |
253 | // condensed is more readable. | |
254 | $buf .= "\n"; | |
255 | } | |
256 | $buf .= $query . " //\n"; | |
257 | } | |
258 | $buf .= "DELIMITER ;\n"; | |
259 | file_put_contents($this->getFile(), $buf, FILE_APPEND); | |
260 | $this->enqueuedQueries = []; | |
261 | } | |
262 | } | |
263 | ||
4ed867e0 | 264 | } |