-
Notifications
You must be signed in to change notification settings - Fork 3
/
DemoGenerator.php
executable file
·343 lines (289 loc) · 13 KB
/
DemoGenerator.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
<?php
namespace DemoGenerator;
/**
* The DemoGenerator class is tasked with dynamically creating demo instances of the default Wordpress
* installation, complete with cloned tables and upload directories.
*/
class DemoGenerator
{
/**
* Demo instance prefix.
*/
const TABLE_PREFIX = 'wpdemo_';
/**
* Demo instance prefix - used to identify demo instances in the database.
*/
const TEMPLATE_PREFIX = 'wp_';
/**
* Upload directory prefix. This is used when creating copies of the default uploads dir.
*/
const UPLOAD_PREFIX = 'wp-content/wpdemo_';
/**
* Default uploads folder, relative to Wordpress root.
*/
const UPLOAD_FOLDER = 'wp-content/uploads';
/**
* Maximum number of instances.
*/
const MAX_INSTANCES = 20;
/**
* Demo instance lifetime, in minutes. Do note that if you're using the default Wordpress cron that it's set to run every hour (the minimum value).
*/
const LIFETIME = 15;
/**
* Singleton object instance. Accessed via getInstance().
*/
private static $instance = null;
/**
* Demo instance ID. This ID, consisting of the TABLE_PREFIX plus a random string,
* is used to uniquely identify the users session. The value is stored on the client side via cookies.
*/
protected $instanceID = null;
/**
* PHP Data Object, used for database operations.
*/
protected $pdo = null;
public function getTablePrefix() {
return self::TABLE_PREFIX . $this->getInstanceInteger() . '_';
}
public function getUploadDir()
{
return self::UPLOAD_PREFIX . $this->getInstanceInteger();
}
private function __construct()
{
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new DemoGenerator();
}
return self::$instance;
}
protected function getInstanceInteger() {
return isset($this->instanceID) ? $this->instanceID : false;
}
/**
* Instantiates the demo session - either by creating a new demo instance (if none exists for this user) or
* by setting the values from an existing one.
* @return void
*/
public function instantiateSession()
{
session_start();
if (isset($_SESSION['instanceID']) === false && defined('DOING_CRON') === false) {
$this->generateDemoInstance();
} else {
$this->instanceID = $_SESSION['instanceID'];
}
}
/**
* Create a demo instance. Generates the instance cookie, clones tables,
* updates prefixes in options and usermeta and clones the upload directory.
* @return void
*/
public function generateDemoInstance()
{
if ($this->countInstances() > self::MAX_INSTANCES) {
die('Too many users are using the demo. Please try again later.');
}
$this->instanceID = $this->generateRandomString();
$_SESSION['instanceID'] = $this->instanceID;
$from = ABSPATH . self::UPLOAD_FOLDER;
$to = ABSPATH . $this->getUploadDir();
$this->importTablesFromTemplate();
$this->cloneUploadsDirectory($from, $to);
}
/**
* Generates a random, 10 character string.
* @return string Random 10 character string.
*/
protected function generateRandomString()
{
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$length = strlen($chars);
$result = '';
for ($i = 0; $i < 10; $i++) {
$result .= $chars[rand(0, $length - 1)];
}
return $result;
}
/**
* Gets a list of all the tables that need to be cloned, based on the default prefix, copies them and fixes the prefixes
* in options and usermeta tables.
* @return void
*/
public function importTablesFromTemplate()
{
//Get a list of all the tables that need to be cloned
$tables = $this->getTablesFromTemplate(self::TEMPLATE_PREFIX);
//Clone the tables from the template into the demo instance
$instancePrefix = $this->getTablePrefix();
$defaultPrefix = self::TEMPLATE_PREFIX;
$this->cloneTables($tables, $defaultPrefix, $instancePrefix);
$this->convertPrefixes($defaultPrefix, $instancePrefix);
}
/**
* Converts the prefixes in the options and usermeta tables to work with the demo instance.
* @param string $defaultPrefix The default database prefix (eg: wp_).
* @param string $instancePrefix The demo instance prefix (eg: wpdemo_abcd123456_)
* @return void
*/
protected function convertPrefixes($defaultPrefix, $instancePrefix)
{
$pdo = $this->getPDO();
$update = "";
//Update options table
$sql = "SELECT option_name FROM ".$defaultPrefix."options WHERE option_name LIKE '".$defaultPrefix."%'";
$query = $pdo->query($sql);
$fields = $query->fetchAll(\PDO::FETCH_COLUMN, 0);
foreach ($fields as $field) {
$replacement = str_replace($defaultPrefix, $instancePrefix, $field);
$update .= "UPDATE ".$instancePrefix."options SET option_name = '".$replacement."' WHERE option_name = '".$field."';";
}
//Update usermeta table
$pdo = $this->getPDO();
$sql = "SELECT meta_key FROM ".$defaultPrefix."usermeta WHERE meta_key LIKE '".$defaultPrefix."%'";
$query = $pdo->query($sql);
$fields = $query->fetchAll(\PDO::FETCH_COLUMN, 0);
foreach ($fields as $field) {
$replacement = str_replace($defaultPrefix, $instancePrefix, $field);
$update .= "UPDATE ".$instancePrefix."usermeta SET meta_key = '".$replacement."' WHERE meta_key = '".$field."';";
}
$pdo->exec($update);
}
/**
* Generates the PDO object (if it doesn't exist) and returns it.
* @return PDO
*/
protected function getPDO()
{
if ($this->pdo === null) {
$dsn = 'mysql:dbname='.DB_NAME.';host='.DB_HOST.';';
$user = DB_USER;
$pass = DB_PASSWORD;
try {
$this->pdo = new \PDO($dsn, $user, $pass, array(\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'));
} catch (Exception $e) {
die ('Failed to access database, WPDemo cannot continue...');
}
}
return $this->pdo;
}
/**
* Clones the tables by name, copying the schema and populating them with data from the original.
* @param array $tables A list of tables to be copied, sans prefix.
* @param string $defaultPrefix The default table prefix (eg: wp_).
* @param string $instancePrefix The new table prefix (eg: wpdemo_abcd123456_)
* @return void
*/
protected function cloneTables($tables, $defaultPrefix, $instancePrefix)
{
$pdo = $this->getPDO();
$query = '';
foreach ($tables as $table) {
$defaultTable = $defaultPrefix . $table;
$instanceTable = $instancePrefix . $table;
$query .= "CREATE TABLE $instanceTable LIKE $defaultTable; INSERT $instanceTable SELECT * FROM $defaultTable;";
}
$pdo->exec($query);
}
/**
* Retrieve a list of tables to be cloned from the default installation and remove the default prefix from them.
* @param string $templatePrefix Default table prefix (eg: wp_)
* @return array List of tables to be copied.
*/
protected function getTablesFromTemplate($templatePrefix)
{
$pdo = $this->getPDO();
$escaped = str_replace('_', '\_', $templatePrefix); //Underscores just happen to be a wildcard in SQL LIKE statements, so we need to work around this
$sql = "SHOW TABLES LIKE '$escaped%'";
$query = $pdo->query($sql);
$tables = $query->fetchAll(\PDO::FETCH_COLUMN, 0);
$results = array_map(function ($tableName) use ($templatePrefix) {return str_replace($templatePrefix, '', $tableName);}, $tables);
return $results;
}
/**
* Recursively clone the uploads directory, icluding all files and sub-directories.
* @return void
*/
protected function cloneUploadsDirectory($from, $to)
{
if (file_exists($from) === false) {
return false;
} else {
mkdir($to, 0755);
foreach (
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($from, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST) as $item
) {
if ($item->isDir()) {
mkdir($to . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
} else {
copy($item, $to . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
}
}
}
}
/**
* Remove expired demo instances by dropping their tables from the database and deleting their uploads folder. This method doesn't really do
* much - it just delegates to other methods.
* @return void
*/
public function cleanupInstances($lifetime = self::LIFETIME)
{
$this->cleanupTables($lifetime, self::TABLE_PREFIX);
$this->cleanupUploadDirs($lifetime, self::UPLOAD_PREFIX);
}
/**
* Remove tables belonging to expired demo instances.
* @param int $olderThan Lifetime, in minutes. Tables older than this will be deleted.
* @param string $tablePrefix Table prefix. Only tables with the demo prefix will be deleted.
* @return void
*/
public function cleanupTables($olderThan, $tablePrefix)
{
$pdo = $this->getPDO();
$escaped = str_replace('_', '\_', $tablePrefix); //Underscores just happen to be a wildcard in SQL LIKE statements, so we need to work around this
$sql = "SHOW TABLE STATUS WHERE name LIKE '$escaped%' AND Create_time < NOW() - INTERVAL $olderThan MINUTE;";
$query = $pdo->query($sql);
$tables = $query->fetchAll(\PDO::FETCH_COLUMN, 0);
$toDrop = array();
foreach ($tables as $table) {
$toDrop[] = $table;
}
$sql = sprintf("DROP TABLE %s;", implode(', ', $toDrop));
$pdo->exec($sql);
}
/**
* Return the number of currently existing instances.
* @return int The number of instances.
*/
public function countInstances() {
$sql = "SHOW TABLES LIKE '" . self::TABLE_PREFIX . "%usermeta'";
$pdo = $this->getPDO();
$query = $pdo->query($sql);
$instances = count( $query->fetch(\PDO::FETCH_NUM) );
return $instances;
}
/**
* Recursively delete upload dir, along with files and subdirectories, ignoring dots.
* @param string $uploadPrefix Upload directory to delete.
* @return void
*/
public function cleanupUploadDirs($lifetime, $uploadPrefix)
{
$uploadDirs = glob(ABSPATH . $uploadPrefix . '*');
$toDelete = array_filter($uploadDirs, function($dir) {
return filemtime($dir) < time() - $lifetime * 60 ? true : false;
});
foreach ($toDelete as $dir) {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($dir . '/', \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST);
foreach ($files as $file)
$file->isDir() === true ? rmdir($file->getRealPath()) : unlink($file->getRealPath());
rmdir($dir);
}
}
}