Skip to content

Commit 748b3d7

Browse files
committed
The original file
1 parent 9b41eae commit 748b3d7

File tree

1 file changed

+376
-0
lines changed

1 file changed

+376
-0
lines changed
Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
<?php
2+
/**
3+
* Contains useful functions for cleaning up the database.
4+
*
5+
* @copyright 2009-2019 Vanilla Forums Inc.
6+
* @license GPL-2.0-only
7+
* @package Dashboard
8+
* @since 2.1
9+
*/
10+
11+
/**
12+
* Database Administration task handler.
13+
*/
14+
class DBAModel extends Gdn_Model {
15+
16+
/** @var int Operations to perform at once. */
17+
public static $ChunkSize = 10000;
18+
19+
/**
20+
* Update the counters.
21+
*
22+
* @param $table
23+
* @param $column
24+
* @param bool $from
25+
* @param bool $to
26+
* @return mixed
27+
* @throws Gdn_UserException
28+
*/
29+
public function counts($table, $column, $from = false, $to = false) {
30+
$model = $this->createModel($table);
31+
32+
if (!method_exists($model, 'Counts')) {
33+
throw new Gdn_UserException("The $table model does not support count recalculation.");
34+
}
35+
36+
$result = $model->counts($column, $from, $to);
37+
return $result;
38+
}
39+
40+
/**
41+
* Create a model for the given table.
42+
*
43+
* @param string $table
44+
* @return Gdn_Model
45+
*/
46+
public function createModel($table) {
47+
$modelName = $table.'Model';
48+
if (class_exists($modelName)) {
49+
return new $modelName();
50+
} else {
51+
return new Gdn_Model($table);
52+
}
53+
}
54+
55+
/**
56+
* Return SQL for updating a count.
57+
*
58+
* @param string $aggregate count, max, min, etc.
59+
* @param string $parentTable The name of the parent table.
60+
* @param string $childTable The name of the child table
61+
* @param string $parentColumnName
62+
* @param string $childColumnName
63+
* @param string $parentJoinColumn
64+
* @param string $childJoinColumn
65+
* @param int|string $default A default value for the field. Passed to MySQL's coalesce function.
66+
* @return string
67+
*/
68+
public static function getCountSQL(
69+
$aggregate,
70+
// count, max, min, etc.
71+
$parentTable,
72+
$childTable,
73+
$parentColumnName = '',
74+
$childColumnName = '',
75+
$parentJoinColumn = '',
76+
$childJoinColumn = '',
77+
$where = [],
78+
$default = 0
79+
) {
80+
81+
$pDO = Gdn::database()->connection();
82+
$default = $pDO->quote($default);
83+
84+
if (!$parentColumnName) {
85+
switch (strtolower($aggregate)) {
86+
case 'count':
87+
$parentColumnName = "Count{$childTable}s";
88+
break;
89+
case 'max':
90+
$parentColumnName = "Last{$childTable}ID";
91+
break;
92+
case 'min':
93+
$parentColumnName = "First{$childTable}ID";
94+
break;
95+
case 'sum':
96+
$parentColumnName = "Sum{$childTable}s";
97+
break;
98+
}
99+
}
100+
101+
if (!$childColumnName) {
102+
$childColumnName = $childTable.'ID';
103+
}
104+
105+
if (!$parentJoinColumn) {
106+
$parentJoinColumn = $parentTable.'ID';
107+
}
108+
if (!$childJoinColumn) {
109+
$childJoinColumn = $parentJoinColumn;
110+
}
111+
112+
$result = "update :_$parentTable p
113+
set p.$parentColumnName = (
114+
select coalesce($aggregate(c.$childColumnName), $default)
115+
from :_$childTable c
116+
where p.$parentJoinColumn = c.$childJoinColumn)";
117+
118+
if (!empty($where)) {
119+
$wheres = [];
120+
foreach ($where as $column => $value) {
121+
$value = $pDO->quote($value);
122+
$wheres[] = "p.`$column` = $value";
123+
}
124+
125+
$result .= "\n where ".implode(" and ", $wheres);
126+
}
127+
128+
$result = str_replace(':_', Gdn::database()->DatabasePrefix, $result);
129+
return $result;
130+
}
131+
132+
/**
133+
* Remove html entities from a column in the database.
134+
*
135+
* @param string $table The name of the table.
136+
* @param array $column The column to decode.
137+
* @param int $limit The number of records to work on.
138+
*/
139+
public function htmlEntityDecode($table, $column, $limit = 100) {
140+
// Construct a model to save the results.
141+
$model = $this->createModel($table);
142+
143+
// Get the data to decode.
144+
$data = $this->SQL
145+
->select($model->PrimaryKey)
146+
->select($column)
147+
->from($table)
148+
->like($column, '&%;', 'both')
149+
->limit($limit)
150+
->get()->resultArray();
151+
152+
$result = [];
153+
$result['Count'] = count($data);
154+
$result['Complete'] = false;
155+
$result['Decoded'] = [];
156+
$result['NotDecoded'] = [];
157+
158+
// Loop through each row in the working set and decode the values.
159+
foreach ($data as $row) {
160+
$value = $row[$column];
161+
$decodedValue = htmlEntityDecode($value);
162+
163+
$item = ['From' => $value, 'To' => $decodedValue];
164+
165+
if ($value != $decodedValue) {
166+
$model->setField($row[$model->PrimaryKey], $column, $decodedValue);
167+
$result['Decoded'] = $item;
168+
} else {
169+
$result['NotDecoded'] = $item;
170+
}
171+
}
172+
$result['Complete'] = $result['Count'] < $limit;
173+
174+
return $result;
175+
}
176+
177+
/**
178+
* Updates a table's InsertUserID values to the system user ID, when invalid.
179+
*
180+
* @param $table The name of table to fix InsertUserID in.
181+
* @return bool|Gdn_DataSet|string
182+
* @throws Exception
183+
*/
184+
public function fixInsertUserID($table) {
185+
return $this->SQL
186+
->update($table)
187+
->set('InsertUserID', Gdn::userModel()->getSystemUserID())
188+
->where('InsertUserID <', 1)
189+
->put();
190+
}
191+
192+
/**
193+
* If any role has no permission records, set Member-like permissions on it.
194+
*
195+
* @return array
196+
*/
197+
public function fixPermissions() {
198+
$roles = RoleModel::roles();
199+
$roleModel = new RoleModel();
200+
$permissionModel = new PermissionModel();
201+
202+
// Find roles missing permission records
203+
foreach ($roles as $roleID => $role) {
204+
$permissions = $this->SQL->select('*')->from('Permission p')
205+
->where('p.RoleID', $roleID)->get()->resultArray();
206+
207+
if (!count($permissions)) {
208+
// Set basic permission record
209+
$defaultRecord = [
210+
'RoleID' => $roleID,
211+
'JunctionTable' => null,
212+
'JunctionColumn' => null,
213+
'JunctionID' => null,
214+
'Garden.Email.View' => 1,
215+
'Garden.SignIn.Allow' => 1,
216+
'Garden.Activity.View' => 1,
217+
'Garden.Profiles.View' => 1,
218+
'Garden.Profiles.Edit' => 1,
219+
'Conversations.Conversations.Add' => 1
220+
];
221+
$permissionModel->save($defaultRecord);
222+
223+
// Set default category permission
224+
$defaultCategory = [
225+
'RoleID' => $roleID,
226+
'JunctionTable' => 'Category',
227+
'JunctionColumn' => 'PermissionCategoryID',
228+
'JunctionID' => -1,
229+
'Vanilla.Discussions.View' => 1,
230+
'Vanilla.Discussions.Add' => 1,
231+
'Vanilla.Comments.Add' => 1
232+
];
233+
$permissionModel->save($defaultCategory);
234+
}
235+
}
236+
237+
return ['Complete' => true];
238+
}
239+
240+
public function fixUrlCodes($table, $column) {
241+
$model = $this->createModel($table);
242+
243+
// Get the data to decode.
244+
$data = $this->SQL
245+
->select($model->PrimaryKey)
246+
->select($column)
247+
->from($table)
248+
// ->like($Column, '&%;', 'both')
249+
// ->limit($Limit)
250+
->get()->resultArray();
251+
252+
foreach ($data as $row) {
253+
$value = $row[$column];
254+
$encoded = Gdn_Format::url($value);
255+
256+
if (!$value || $value != $encoded) {
257+
$model->setField($row[$model->PrimaryKey], $column, $encoded);
258+
Gdn::controller()->Data['Encoded'][$row[$model->PrimaryKey]] = $encoded;
259+
}
260+
}
261+
262+
return ['Complete' => true];
263+
}
264+
265+
/**
266+
* Apply the specified RoleID to all users without a valid role
267+
*
268+
* @param $roleID
269+
* @return bool|Gdn_DataSet|string
270+
* @throws Exception
271+
*/
272+
public function fixUserRole($roleID) {
273+
$pDO = Gdn::database()->connection();
274+
$insertQuery = "
275+
insert into :_UserRole
276+
277+
select u.UserID, ".$pDO->quote($roleID)." as RoleID
278+
from :_User u
279+
left join :_UserRole ur on u.UserID = ur.UserID
280+
left join :_Role r on ur.RoleID = r.RoleID
281+
where r.Name is null";
282+
$insertQuery = str_replace(':_', Gdn::database()->DatabasePrefix, $insertQuery);
283+
return $this->SQL->query($insertQuery);
284+
}
285+
286+
/**
287+
*
288+
*
289+
* @param $table
290+
* @param $key
291+
*/
292+
public function resetBatch($table, $key) {
293+
$key = "DBA.Range.$key";
294+
Gdn::set($key, null);
295+
}
296+
297+
/**
298+
*
299+
*
300+
* @param $table
301+
* @param $key
302+
* @param int $limit
303+
* @param bool $max
304+
* @return array|mixed
305+
*/
306+
public function getBatch($table, $key, $limit = 10000, $max = false) {
307+
$key = "DBA.Range.$key";
308+
309+
// See if there is already a range.
310+
$current = dbdecode(Gdn::get($key, ''));
311+
if (!is_array($current) || !isset($current['Min']) || !isset($current['Max'])) {
312+
list($current['Min'], $current['Max']) = $this->primaryKeyRange($table);
313+
314+
if ($max && $current['Max'] > $max) {
315+
$current['Max'] = $max;
316+
}
317+
}
318+
319+
if (!isset($current['To'])) {
320+
$current['To'] = $current['Max'];
321+
} else {
322+
$current['To'] -= $limit - 1;
323+
}
324+
$current['From'] = $current['To'] - $limit;
325+
Gdn::set($key, dbencode($current));
326+
$current['Complete'] = $current['To'] < $current['Min'];
327+
328+
$total = $current['Max'] - $current['Min'];
329+
if ($total > 0) {
330+
$complete = $current['Max'] - $current['From'];
331+
332+
$percent = 100 * $complete / $total;
333+
if ($percent > 100) {
334+
$percent = 100;
335+
}
336+
$current['Percent'] = round($percent).'%';
337+
}
338+
339+
return $current;
340+
}
341+
342+
/**
343+
* Return the min and max values of a table's primary key.
344+
*
345+
* @param string $table The name of the table to look at.
346+
* @return array An array in the form (min, max).
347+
*/
348+
public function primaryKeyRange($table) {
349+
$model = $this->createModel($table);
350+
351+
$data = $this->SQL
352+
->select($model->PrimaryKey, 'min', 'MinValue')
353+
->select($model->PrimaryKey, 'max', 'MaxValue')
354+
->from($table)
355+
->get()->firstRow(DATASET_TYPE_ARRAY);
356+
357+
if ($data) {
358+
return [$data['MinValue'], $data['MaxValue']];
359+
} else {
360+
return [0, 0];
361+
}
362+
}
363+
364+
/**
365+
* Perform basic validation on a database identifier name.
366+
*
367+
* @link https://dev.mysql.com/doc/refman/5.6/en/identifiers.html Identifier name specification.
368+
* @param string $string A value to be used as a database identifier.
369+
* @return bool True if valid, otherwise false.
370+
*/
371+
public function isValidDatabaseIdentifier($string) {
372+
// Sticking to ASCII.
373+
$result = (bool)preg_match('/^(?![0-9]+$)[0-9a-zA-Z$_]+$/', $string);
374+
return $result;
375+
}
376+
}

0 commit comments

Comments
 (0)