WP Reset – Fastest WordPress Reset Plugin - Version 1.40

Version Description

Download this release

Release Info

Developer WebFactory
Plugin Icon 128x128 WP Reset – Fastest WordPress Reset Plugin
Version 1.40
Comparing to
See all releases

Code changes from version 1.35 to 1.40

css/wp-reset.css CHANGED
@@ -18,7 +18,9 @@
18
  text-align: center;
19
  }
20
 
21
- .tools_page_wp-reset.wp-core-ui .button, .tools_page_wp-reset.wp-core-ui .button-primary, .tools_page_wp-reset.wp-core-ui .button-secondary {
 
 
22
  border-radius: 0;
23
  }
24
 
@@ -99,9 +101,12 @@
99
 
100
  .tools_page_wp-reset .card {
101
  padding: 1em 2em 1em 2em;
 
102
  }
103
 
104
- .tools_page_wp-reset .card.collapsed p, .tools_page_wp-reset .card.collapsed b, .tools_page_wp-reset .card.collapsed ul {
 
 
105
  display: none;
106
  }
107
 
@@ -109,6 +114,137 @@
109
  border-radius: 0;
110
  }
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  @-moz-keyframes spin {
113
  100% {
114
  -moz-transform: rotate(-360deg);
@@ -196,7 +332,7 @@
196
  font-weight: 600;
197
  line-height: 32px;
198
  color: #AAAAAA;
199
- padding: 10px 25px;
200
  display: block;
201
  letter-spacing: 1px;
202
  }
@@ -221,7 +357,7 @@
221
  display: inline-block;
222
  font-size: 12px;
223
  line-height: 16px;
224
- margin: 0px 15px 0px 6px;
225
  text-decoration: none;
226
  text-shadow: none;
227
  background: rgba(255, 255, 255, 0.7);
@@ -265,3 +401,93 @@
265
  -webkit-box-shadow: none;
266
  }
267
  /* tabs */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  text-align: center;
19
  }
20
 
21
+ .tools_page_wp-reset.wp-core-ui .button,
22
+ .tools_page_wp-reset.wp-core-ui .button-primary,
23
+ .tools_page_wp-reset.wp-core-ui .button-secondary {
24
  border-radius: 0;
25
  }
26
 
101
 
102
  .tools_page_wp-reset .card {
103
  padding: 1em 2em 1em 2em;
104
+ width: 520px;
105
  }
106
 
107
+ .tools_page_wp-reset .card.collapsed p,
108
+ .tools_page_wp-reset .card.collapsed b,
109
+ .tools_page_wp-reset .card.collapsed ul {
110
  display: none;
111
  }
112
 
114
  border-radius: 0;
115
  }
116
 
117
+ .tools_page_wp-reset .create-new-snapshot-corner {
118
+ position: absolute;
119
+ top: 2em;
120
+ right: 2em;
121
+ }
122
+
123
+ .tools_page_wp-reset table {
124
+ width: calc(100% + 4em);
125
+ border-spacing: 0;
126
+ border-collapse: separate;
127
+ margin: 0 0 0 -2em;
128
+ }
129
+
130
+ .tools_page_wp-reset table td {
131
+ text-align: left;
132
+ padding: 5px;
133
+ }
134
+
135
+ .tools_page_wp-reset table tr:nth-child(even) td {
136
+ background-color: #f9f9f9;
137
+ }
138
+
139
+ .tools_page_wp-reset table .ss-actions {
140
+ width: 55px;
141
+ }
142
+
143
+ .tools_page_wp-reset table th {
144
+ text-align: left;
145
+ border-bottom: thin solid #444444;
146
+ padding: 5px;
147
+ }
148
+
149
+ .tools_page_wp-reset table .ss-action {
150
+ text-decoration: none;
151
+ margin: 0 7px 5px 0;
152
+ display: inline-block;
153
+ }
154
+
155
+ .tools_page_wp-reset table .delete-snapshot {
156
+ margin: 0;
157
+ }
158
+
159
+ .tools_page_wp-reset .no-padding-bottom {
160
+ padding-bottom: 0;
161
+ }
162
+
163
+ .wpr-table-container {
164
+ min-width: 960px;
165
+ max-width: 1200px;
166
+ margin: 0 auto 15px auto;
167
+ }
168
+
169
+ .swal2-popup.compare-snapshots {
170
+ min-height: 90%;
171
+ flex-direction: column;
172
+ justify-content: flex-start;
173
+ }
174
+
175
+ .wpr-table-container pre {
176
+ font-size: 14px;
177
+ overflow: auto;
178
+ max-width: 580px;
179
+ }
180
+
181
+ .wpr-table-container p {
182
+ font-size: 14px;
183
+ color: #545454;
184
+ }
185
+
186
+ .wpr-table-container table {
187
+ border-spacing: 0;
188
+ border-collapse: separate;
189
+ margin: 0;
190
+ padding: 0;
191
+ width: 100%;
192
+ position: relative;
193
+ }
194
+
195
+ .wpr-table-container table span.dashicons {
196
+ position: absolute;
197
+ right: 10px;
198
+ top: 13px;
199
+ }
200
+
201
+ .wpr-table-container table td {
202
+ width: 50%;
203
+ padding: 10px;
204
+ }
205
+
206
+ .wpr-table-container > table > tbody > tr:first-child {
207
+ cursor: pointer;
208
+ }
209
+
210
+ .wpr-table-container table tr td:nth-child(2) {
211
+ border-left: thin solid #00000080;
212
+ }
213
+
214
+ .wpr-table-container table td {
215
+ background-color: #f9f9f9;
216
+ }
217
+
218
+ .wpr-table-container .no-padding {
219
+ padding: 0;
220
+ }
221
+
222
+ .wpr-table-container .wpr-table-missing td {
223
+ background-color: #ff000830;
224
+ }
225
+
226
+ .wpr-table-container .wpr-table-difference td {
227
+ background-color: rgba(255, 166, 0, 0.30);
228
+ }
229
+
230
+ .wpr-table-container .wpr-table-match td {
231
+ background-color: rgba(9, 255, 0, 0.30);
232
+ }
233
+
234
+ .wpr-table-container table.table_diff tr td {
235
+ width: auto;
236
+ border-left: none;
237
+ word-break: break-all;
238
+ font-size: 14px;
239
+ font-family: monospace;
240
+ vertical-align: top;
241
+ }
242
+
243
+ .wpr-table-container .table_diff {
244
+ width: 100%;
245
+ }
246
+
247
+
248
  @-moz-keyframes spin {
249
  100% {
250
  -moz-transform: rotate(-360deg);
332
  font-weight: 600;
333
  line-height: 32px;
334
  color: #AAAAAA;
335
+ padding: 5px 10px;
336
  display: block;
337
  letter-spacing: 1px;
338
  }
357
  display: inline-block;
358
  font-size: 12px;
359
  line-height: 16px;
360
+ margin: 0px 5px 0px 5px;
361
  text-decoration: none;
362
  text-shadow: none;
363
  background: rgba(255, 255, 255, 0.7);
401
  -webkit-box-shadow: none;
402
  }
403
  /* tabs */
404
+
405
+ /* diff */
406
+
407
+ .Differences {
408
+ width: 100%;
409
+ border-collapse: collapse;
410
+ border-spacing: 0;
411
+ empty-cells: show;
412
+ }
413
+
414
+ .Differences thead th {
415
+ text-align: left;
416
+ border-bottom: 1px solid #000;
417
+ background: #aaa;
418
+ color: #000;
419
+ padding: 4px;
420
+ }
421
+ .Differences tbody th {
422
+ text-align: right;
423
+ background: #ccc;
424
+ width: 4em;
425
+ padding: 1px 2px;
426
+ border-right: 1px solid #00000080;
427
+ border-bottom: none;
428
+ vertical-align: middle;
429
+ font-size: 13px;
430
+ font-weight: 400;
431
+ }
432
+
433
+ .Differences td {
434
+ padding: 5px !important;
435
+ font-family: Consolas, monospace;
436
+ font-size: 13px;
437
+ }
438
+
439
+ .DifferencesSideBySide .ChangeInsert td.Left {
440
+ background: #dfd;
441
+ }
442
+
443
+ .DifferencesSideBySide .ChangeInsert td.Right {
444
+ background: #cfc;
445
+ }
446
+
447
+ .DifferencesSideBySide .ChangeDelete td.Left {
448
+ background: #f88;
449
+ }
450
+
451
+ .DifferencesSideBySide .ChangeDelete td.Right {
452
+ background: #faa;
453
+ }
454
+
455
+ .DifferencesSideBySide .ChangeReplace .Left {
456
+ background: #fe9;
457
+ }
458
+
459
+ .DifferencesSideBySide .ChangeReplace .Right {
460
+ background: #fd8;
461
+ }
462
+
463
+ .Differences ins, .Differences del {
464
+ text-decoration: none;
465
+ }
466
+
467
+ .DifferencesSideBySide .ChangeReplace ins, .DifferencesSideBySide .ChangeReplace del {
468
+ background: #fc0;
469
+ }
470
+
471
+ .Differences .Skipped {
472
+ background: #f7f7f7;
473
+ }
474
+
475
+ .DifferencesInline .ChangeReplace .Left,
476
+ .DifferencesInline .ChangeDelete .Left {
477
+ background: #fdd;
478
+ }
479
+
480
+ .DifferencesInline .ChangeReplace .Right,
481
+ .DifferencesInline .ChangeInsert .Right {
482
+ background: #dfd;
483
+ }
484
+
485
+ .DifferencesInline .ChangeReplace ins {
486
+ background: #9e9;
487
+ }
488
+
489
+ .DifferencesInline .ChangeReplace del {
490
+ background: #e99;
491
+ }
492
+
493
+ /* diff */
js/wp-reset.js CHANGED
@@ -55,7 +55,161 @@ jQuery(document).ready(function($) {
55
  }); // delete plugins
56
 
57
 
58
- function run_tool(button, tool_name) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  confirm_action(wp_reset.confirm_title, $(button).data('text-confirm'), $(button).data('btn-confirm'), wp_reset.cancel_button)
60
  .then((result) => {
61
  if (result.value) {
@@ -65,16 +219,28 @@ jQuery(document).ready(function($) {
65
  data: {
66
  action: 'wp_reset_run_tool',
67
  _ajax_nonce: wp_reset.nonce_run_tool,
68
- tool: tool_name
 
69
  }
70
  }).always(function(data) {
71
  swal.close();
72
  }).done(function(data) {
73
  if (data.success) {
74
  msg = $(button).data('text-done').replace('%n', data.data);
75
- swal({ type: 'success', title: msg });
 
 
 
 
 
 
 
 
 
 
 
76
  } else {
77
- swal({ type: 'error', title: wp_reset.undocumented_error });
78
  }
79
  }).fail(function(data) {
80
  swal({ type: 'error', title: wp_reset.undocumented_error });
55
  }); // delete plugins
56
 
57
 
58
+ // compare snapshot
59
+ $('#wpr-snapshots').on('click', '.compare-snapshot', 'click', function(e) {
60
+ e.preventDefault();
61
+ uid = $(this).data('ss-uid');
62
+ button = $(this);
63
+
64
+ block_ui($(button).data('wait-msg'));
65
+ $.get({
66
+ url: ajaxurl,
67
+ data: {
68
+ action: 'wp_reset_run_tool',
69
+ _ajax_nonce: wp_reset.nonce_run_tool,
70
+ tool: 'compare_snapshots',
71
+ extra_data: uid
72
+ }
73
+ }).always(function(data) {
74
+ swal.close();
75
+ }).done(function(data) {
76
+ if (data.success) {
77
+ msg = $(button).data('title').replace('%s', $(button).data('name'));
78
+ swal({
79
+ width: '90%',
80
+ title: msg,
81
+ html: data.data,
82
+ showConfirmButton: false,
83
+ allowEnterKey:false,
84
+ focusConfirm: false,
85
+ showCloseButton: true,
86
+ customClass: 'compare-snapshots'
87
+ });
88
+ } else {
89
+ swal({ type: 'error', title: wp_reset.documented_error + ' ' + data.data });
90
+ }
91
+ }).fail(function(data) {
92
+ swal({ type: 'error', title: wp_reset.undocumented_error });
93
+ });
94
+
95
+ return false;
96
+ }); // compare snapshot
97
+
98
+
99
+ // restore snapshot
100
+ $('#wpr-snapshots').on('click', '.restore-snapshot', 'click', function(e) {
101
+ e.preventDefault();
102
+ uid = $(this).data('ss-uid');
103
+
104
+ run_tool(this, 'restore_snapshot', uid);
105
+
106
+ return false;
107
+ }); // restore snapshot
108
+
109
+
110
+ // download snapshot
111
+ $('#wpr-snapshots').on('click', '.download-snapshot', 'click', function(e) {
112
+ e.preventDefault();
113
+ uid = $(this).data('ss-uid');
114
+ button = this;
115
+
116
+ block_ui($(this).data('wait-msg'));
117
+ $.get({
118
+ url: ajaxurl,
119
+ data: {
120
+ action: 'wp_reset_run_tool',
121
+ _ajax_nonce: wp_reset.nonce_run_tool,
122
+ tool: 'download_snapshot',
123
+ extra_data: uid
124
+ }
125
+ }).always(function(data) {
126
+ swal.close();
127
+ }).done(function(data) {
128
+ if (data.success) {
129
+ msg = $(button).data('success-msg').replace('%s', data.data);
130
+ swal({ type: 'success', title: msg });
131
+ } else {
132
+ swal({ type: 'error', title: wp_reset.documented_error + ' ' + data.data });
133
+ }
134
+ }).fail(function(data) {
135
+ swal({ type: 'error', title: wp_reset.undocumented_error });
136
+ });
137
+
138
+ return false;
139
+ }); // downlod snapshot
140
+
141
+
142
+ // delete snapshot
143
+ $('#wpr-snapshots').on('click', '.delete-snapshot', 'click', function(e) {
144
+ e.preventDefault();
145
+ uid = $(this).data('ss-uid');
146
+
147
+ run_tool(this, 'delete_snapshot', uid);
148
+
149
+ return false;
150
+ }); // delete snapshot
151
+
152
+
153
+ // create snapshot
154
+ $('.tools_page_wp-reset').on('click', '.create-new-snapshot', 'click', function(e) {
155
+ e.preventDefault();
156
+ button = $('#create-new-snapshot-primary');
157
+
158
+ swal({ title: $(button).data('title'),
159
+ type: 'question',
160
+ text: $(button).data('text'),
161
+ input: 'text',
162
+ inputPlaceholder: $(button).data('placeholder'),
163
+ showCancelButton: true,
164
+ focusConfirm: false,
165
+ confirmButtonText: $(button).data('btn-confirm'),
166
+ cancelButtonText: wp_reset.cancel_button,
167
+ width: 600
168
+ }).then((result) => {
169
+ if (typeof result.value != 'undefined') {
170
+ block = block_ui($(button).data('msg-wait'));
171
+ $.get({
172
+ url: ajaxurl,
173
+ data: {
174
+ action: 'wp_reset_run_tool',
175
+ _ajax_nonce: wp_reset.nonce_run_tool,
176
+ tool: 'create_snapshot',
177
+ extra_data: result.value
178
+ }
179
+ }).always(function(data) {
180
+ swal.close();
181
+ }).done(function(data) {
182
+ if (data.success) {
183
+ swal({ type: 'success', title: $(button).data('msg-success') }).then((result) => {
184
+ location.reload();
185
+ });
186
+ } else {
187
+ swal({ type: 'error', title: wp_reset.documented_error + ' ' + data.data });
188
+ }
189
+ }).fail(function(data) {
190
+ swal({ type: 'error', title: wp_reset.undocumented_error });
191
+ });
192
+ } // if confirmed
193
+ });
194
+
195
+ return false;
196
+ }); // create snapshot
197
+
198
+ // show/hide extra table info in snapshot diff
199
+ $('body.tools_page_wp-reset').on('click', '.header-row', function(e) {
200
+ e.preventDefault();
201
+
202
+ parent = $(this).parents('div.wpr-table-container > table > tbody');
203
+ $(' > tr:not(.header-row)', parent).toggleClass('hidden');
204
+
205
+ $('span.dashicons', parent).toggleClass('dashicons-arrow-down-alt2').toggleClass('dashicons-arrow-up-alt2');
206
+
207
+ return false;
208
+ }); // show hide extra info in diff
209
+
210
+
211
+ // standard way of running a tool, with confirmation, loading and success message
212
+ function run_tool(button, tool_name, extra_data) {
213
  confirm_action(wp_reset.confirm_title, $(button).data('text-confirm'), $(button).data('btn-confirm'), wp_reset.cancel_button)
214
  .then((result) => {
215
  if (result.value) {
219
  data: {
220
  action: 'wp_reset_run_tool',
221
  _ajax_nonce: wp_reset.nonce_run_tool,
222
+ tool: tool_name,
223
+ extra_data: extra_data
224
  }
225
  }).always(function(data) {
226
  swal.close();
227
  }).done(function(data) {
228
  if (data.success) {
229
  msg = $(button).data('text-done').replace('%n', data.data);
230
+ swal({ type: 'success', title: msg }).then(() => {
231
+ if (tool_name == 'restore_snapshot') {
232
+ location.reload();
233
+ }
234
+ });
235
+ if (tool_name == 'delete_snapshot') {
236
+ $('#wpr-ss-' + extra_data).remove();
237
+ if ($('#wpr-snapshots tr').length <= 1) {
238
+ $('#wpr-snapshots').hide();
239
+ $('#ss-no-snapshots').show();
240
+ }
241
+ }
242
  } else {
243
+ swal({ type: 'error', title: wp_reset.documented_error + ' ' + data.data });
244
  }
245
  }).fail(function(data) {
246
  swal({ type: 'error', title: wp_reset.undocumented_error });
libs/diff.php ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Diff
4
+ *
5
+ * A comprehensive library for generating differences between two strings
6
+ * in multiple formats (unified, side by side HTML etc)
7
+ *
8
+ * PHP version 5
9
+ *
10
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
11
+ *
12
+ * All rights reserved.
13
+ *
14
+ * Redistribution and use in source and binary forms, with or without
15
+ * modification, are permitted provided that the following conditions are met:
16
+ *
17
+ * - Redistributions of source code must retain the above copyright notice,
18
+ * this list of conditions and the following disclaimer.
19
+ * - Redistributions in binary form must reproduce the above copyright notice,
20
+ * this list of conditions and the following disclaimer in the documentation
21
+ * and/or other materials provided with the distribution.
22
+ * - Neither the name of the Chris Boulton nor the names of its contributors
23
+ * may be used to endorse or promote products derived from this software
24
+ * without specific prior written permission.
25
+ *
26
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
27
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
28
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
30
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
31
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
32
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
35
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
36
+ * POSSIBILITY OF SUCH DAMAGE.
37
+ *
38
+ * @package Diff
39
+ * @author Chris Boulton <chris.boulton@interspire.com>
40
+ * @copyright (c) 2009 Chris Boulton
41
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
42
+ * @version 1.1
43
+ * @link http://github.com/chrisboulton/php-diff
44
+ */
45
+
46
+ class Diff
47
+ {
48
+ /**
49
+ * @var array The "old" sequence to use as the basis for the comparison.
50
+ */
51
+ private $a = null;
52
+
53
+ /**
54
+ * @var array The "new" sequence to generate the changes for.
55
+ */
56
+ private $b = null;
57
+
58
+ /**
59
+ * @var array Array containing the generated opcodes for the differences between the two items.
60
+ */
61
+ private $groupedCodes = null;
62
+
63
+ /**
64
+ * @var array Associative array of the default options available for the diff class and their default value.
65
+ */
66
+ private $defaultOptions = array(
67
+ 'context' => 3,
68
+ 'ignoreNewLines' => false,
69
+ 'ignoreWhitespace' => false,
70
+ 'ignoreCase' => false
71
+ );
72
+
73
+ /**
74
+ * @var array Array of the options that have been applied for generating the diff.
75
+ */
76
+ private $options = array();
77
+
78
+ /**
79
+ * The constructor.
80
+ *
81
+ * @param array $a Array containing the lines of the first string to compare.
82
+ * @param array $b Array containing the lines for the second string to compare.
83
+ */
84
+ public function __construct($a, $b, $options=array())
85
+ {
86
+ $this->a = $a;
87
+ $this->b = $b;
88
+
89
+ if (is_array($options))
90
+ $this->options = array_merge($this->defaultOptions, $options);
91
+ else
92
+ $this->options = $this->defaultOptions;
93
+ }
94
+
95
+ /**
96
+ * Render a diff using the supplied rendering class and return it.
97
+ *
98
+ * @param object $renderer An instance of the rendering object to use for generating the diff.
99
+ * @return mixed The generated diff. Exact return value depends on the rendered.
100
+ */
101
+ public function render(Diff_Renderer_Abstract $renderer)
102
+ {
103
+ $renderer->diff = $this;
104
+ return $renderer->render();
105
+ }
106
+
107
+ /**
108
+ * Get a range of lines from $start to $end from the first comparison string
109
+ * and return them as an array. If no values are supplied, the entire string
110
+ * is returned. It's also possible to specify just one line to return only
111
+ * that line.
112
+ *
113
+ * @param int $start The starting number.
114
+ * @param int $end The ending number. If not supplied, only the item in $start will be returned.
115
+ * @return array Array of all of the lines between the specified range.
116
+ */
117
+ public function getA($start=0, $end=null)
118
+ {
119
+ if($start == 0 && $end === null) {
120
+ return $this->a;
121
+ }
122
+
123
+ if($end === null) {
124
+ $length = 1;
125
+ }
126
+ else {
127
+ $length = $end - $start;
128
+ }
129
+
130
+ return array_slice($this->a, $start, $length);
131
+
132
+ }
133
+
134
+ /**
135
+ * Get a range of lines from $start to $end from the second comparison string
136
+ * and return them as an array. If no values are supplied, the entire string
137
+ * is returned. It's also possible to specify just one line to return only
138
+ * that line.
139
+ *
140
+ * @param int $start The starting number.
141
+ * @param int $end The ending number. If not supplied, only the item in $start will be returned.
142
+ * @return array Array of all of the lines between the specified range.
143
+ */
144
+ public function getB($start=0, $end=null)
145
+ {
146
+ if($start == 0 && $end === null) {
147
+ return $this->b;
148
+ }
149
+
150
+ if($end === null) {
151
+ $length = 1;
152
+ }
153
+ else {
154
+ $length = $end - $start;
155
+ }
156
+
157
+ return array_slice($this->b, $start, $length);
158
+ }
159
+
160
+ /**
161
+ * Generate a list of the compiled and grouped opcodes for the differences between the
162
+ * two strings. Generally called by the renderer, this class instantiates the sequence
163
+ * matcher and performs the actual diff generation and return an array of the opcodes
164
+ * for it. Once generated, the results are cached in the diff class instance.
165
+ *
166
+ * @return array Array of the grouped opcodes for the generated diff.
167
+ */
168
+ public function getGroupedOpcodes()
169
+ {
170
+ if(!is_null($this->groupedCodes)) {
171
+ return $this->groupedCodes;
172
+ }
173
+
174
+ require_once dirname(__FILE__).'/Diff/SequenceMatcher.php';
175
+ $sequenceMatcher = new Diff_SequenceMatcher($this->a, $this->b, null, $this->options);
176
+ $this->groupedCodes = $sequenceMatcher->getGroupedOpcodes($this->options['context']);
177
+ return $this->groupedCodes;
178
+ }
179
+ }
libs/diff/Renderer/Abstract.php ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Abstract class for diff renderers in PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ abstract class Diff_Renderer_Abstract
44
+ {
45
+ /**
46
+ * @var object Instance of the diff class that this renderer is generating the rendered diff for.
47
+ */
48
+ public $diff;
49
+
50
+ /**
51
+ * @var array Array of the default options that apply to this renderer.
52
+ */
53
+ protected $defaultOptions = array();
54
+
55
+ /**
56
+ * @var array Array containing the user applied and merged default options for the renderer.
57
+ */
58
+ protected $options = array();
59
+
60
+ /**
61
+ * The constructor. Instantiates the rendering engine and if options are passed,
62
+ * sets the options for the renderer.
63
+ *
64
+ * @param array $options Optionally, an array of the options for the renderer.
65
+ */
66
+ public function __construct(array $options = array())
67
+ {
68
+ $this->setOptions($options);
69
+ }
70
+
71
+ /**
72
+ * Set the options of the renderer to those supplied in the passed in array.
73
+ * Options are merged with the default to ensure that there aren't any missing
74
+ * options.
75
+ *
76
+ * @param array $options Array of options to set.
77
+ */
78
+ public function setOptions(array $options)
79
+ {
80
+ $this->options = array_merge($this->defaultOptions, $options);
81
+ }
82
+ }
libs/diff/Renderer/Html/Array.php ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Base renderer for rendering HTML based diffs for PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ require_once dirname(__FILE__).'/../Abstract.php';
44
+
45
+ class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract
46
+ {
47
+ /**
48
+ * @var array Array of the default options that apply to this renderer.
49
+ */
50
+ protected $defaultOptions = array(
51
+ 'tabSize' => 4
52
+ );
53
+
54
+ /**
55
+ * Render and return an array structure suitable for generating HTML
56
+ * based differences. Generally called by subclasses that generate a
57
+ * HTML based diff and return an array of the changes to show in the diff.
58
+ *
59
+ * @return array An array of the generated chances, suitable for presentation in HTML.
60
+ */
61
+ public function render()
62
+ {
63
+ // As we'll be modifying a & b to include our change markers,
64
+ // we need to get the contents and store them here. That way
65
+ // we're not going to destroy the original data
66
+ $a = $this->diff->getA();
67
+ $b = $this->diff->getB();
68
+
69
+ $changes = array();
70
+ $opCodes = $this->diff->getGroupedOpcodes();
71
+ foreach($opCodes as $group) {
72
+ $blocks = array();
73
+ $lastTag = null;
74
+ $lastBlock = 0;
75
+ foreach($group as $code) {
76
+ list($tag, $i1, $i2, $j1, $j2) = $code;
77
+
78
+ if($tag == 'replace' && $i2 - $i1 == $j2 - $j1) {
79
+ for($i = 0; $i < ($i2 - $i1); ++$i) {
80
+ $fromLine = $a[$i1 + $i];
81
+ $toLine = $b[$j1 + $i];
82
+
83
+ list($start, $end) = $this->getChangeExtent($fromLine, $toLine);
84
+ if($start != 0 || $end != 0) {
85
+ $last = $end + strlen($fromLine);
86
+ $fromLine = substr_replace($fromLine, "\0", $start, 0);
87
+ $fromLine = substr_replace($fromLine, "\1", $last + 1, 0);
88
+ $last = $end + strlen($toLine);
89
+ $toLine = substr_replace($toLine, "\0", $start, 0);
90
+ $toLine = substr_replace($toLine, "\1", $last + 1, 0);
91
+ $a[$i1 + $i] = $fromLine;
92
+ $b[$j1 + $i] = $toLine;
93
+ }
94
+ }
95
+ }
96
+
97
+ if($tag != $lastTag) {
98
+ $blocks[] = array(
99
+ 'tag' => $tag,
100
+ 'base' => array(
101
+ 'offset' => $i1,
102
+ 'lines' => array()
103
+ ),
104
+ 'changed' => array(
105
+ 'offset' => $j1,
106
+ 'lines' => array()
107
+ )
108
+ );
109
+ $lastBlock = count($blocks)-1;
110
+ }
111
+
112
+ $lastTag = $tag;
113
+
114
+ if($tag == 'equal') {
115
+ $lines = array_slice($a, $i1, ($i2 - $i1));
116
+ $blocks[$lastBlock]['base']['lines'] += $this->formatLines($lines);
117
+ $lines = array_slice($b, $j1, ($j2 - $j1));
118
+ $blocks[$lastBlock]['changed']['lines'] += $this->formatLines($lines);
119
+ }
120
+ else {
121
+ if($tag == 'replace' || $tag == 'delete') {
122
+ $lines = array_slice($a, $i1, ($i2 - $i1));
123
+ $lines = $this->formatLines($lines);
124
+ $lines = str_replace(array("\0", "\1"), array('<del>', '</del>'), $lines);
125
+ $blocks[$lastBlock]['base']['lines'] += $lines;
126
+ }
127
+
128
+ if($tag == 'replace' || $tag == 'insert') {
129
+ $lines = array_slice($b, $j1, ($j2 - $j1));
130
+ $lines = $this->formatLines($lines);
131
+ $lines = str_replace(array("\0", "\1"), array('<ins>', '</ins>'), $lines);
132
+ $blocks[$lastBlock]['changed']['lines'] += $lines;
133
+ }
134
+ }
135
+ }
136
+ $changes[] = $blocks;
137
+ }
138
+ return $changes;
139
+ }
140
+
141
+ /**
142
+ * Given two strings, determine where the changes in the two strings
143
+ * begin, and where the changes in the two strings end.
144
+ *
145
+ * @param string $fromLine The first string.
146
+ * @param string $toLine The second string.
147
+ * @return array Array containing the starting position (0 by default) and the ending position (-1 by default)
148
+ */
149
+ private function getChangeExtent($fromLine, $toLine)
150
+ {
151
+ $start = 0;
152
+ $limit = min(strlen($fromLine), strlen($toLine));
153
+ while($start < $limit && $fromLine{$start} == $toLine{$start}) {
154
+ ++$start;
155
+ }
156
+ $end = -1;
157
+ $limit = $limit - $start;
158
+ while(-$end <= $limit && substr($fromLine, $end, 1) == substr($toLine, $end, 1)) {
159
+ --$end;
160
+ }
161
+ return array(
162
+ $start,
163
+ $end + 1
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Format a series of lines suitable for output in a HTML rendered diff.
169
+ * This involves replacing tab characters with spaces, making the HTML safe
170
+ * for output, ensuring that double spaces are replaced with &nbsp; etc.
171
+ *
172
+ * @param array $lines Array of lines to format.
173
+ * @return array Array of the formatted lines.
174
+ */
175
+ protected function formatLines($lines)
176
+ {
177
+ $lines = array_map(array($this, 'ExpandTabs'), $lines);
178
+ $lines = array_map(array($this, 'HtmlSafe'), $lines);
179
+ foreach($lines as &$line) {
180
+ $line = preg_replace_callback('# ( +)|^ #', array($this, 'fixSpaces'), $line);
181
+ }
182
+ return $lines;
183
+ }
184
+
185
+ /**
186
+ * Replace a string containing spaces with a HTML representation using &nbsp;.
187
+ *
188
+ * @param string[] $matches Array with preg matches.
189
+ * @return string The HTML representation of the string.
190
+ */
191
+ private function fixSpaces(array $matches)
192
+ {
193
+ $spaces = $matches[1];
194
+ $count = strlen($spaces);
195
+ if($count == 0) {
196
+ return '';
197
+ }
198
+
199
+ $div = floor($count / 2);
200
+ $mod = $count % 2;
201
+ return str_repeat('&nbsp; ', $div).str_repeat('&nbsp;', $mod);
202
+ }
203
+
204
+ /**
205
+ * Replace tabs in a single line with a number of spaces as defined by the tabSize option.
206
+ *
207
+ * @param string $line The containing tabs to convert.
208
+ * @return string The line with the tabs converted to spaces.
209
+ */
210
+ private function expandTabs($line)
211
+ {
212
+ return str_replace("\t", str_repeat(' ', $this->options['tabSize']), $line);
213
+ }
214
+
215
+ /**
216
+ * Make a string containing HTML safe for output on a page.
217
+ *
218
+ * @param string $string The string.
219
+ * @return string The string with the HTML characters replaced by entities.
220
+ */
221
+ private function htmlSafe($string)
222
+ {
223
+ return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8');
224
+ }
225
+ }
libs/diff/Renderer/Html/Inline.php ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Inline HTML diff generator for PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ require_once dirname(__FILE__).'/Array.php';
44
+
45
+ class Diff_Renderer_Html_Inline extends Diff_Renderer_Html_Array
46
+ {
47
+ /**
48
+ * Render a and return diff with changes between the two sequences
49
+ * displayed inline (under each other)
50
+ *
51
+ * @return string The generated inline diff.
52
+ */
53
+ public function render()
54
+ {
55
+ $changes = parent::render();
56
+ $html = '';
57
+ if(empty($changes)) {
58
+ return $html;
59
+ }
60
+
61
+ $html .= '<table class="Differences DifferencesInline">';
62
+ $html .= '<thead>';
63
+ $html .= '<tr>';
64
+ $html .= '<th>Old</th>';
65
+ $html .= '<th>New</th>';
66
+ $html .= '<th>Differences</th>';
67
+ $html .= '</tr>';
68
+ $html .= '</thead>';
69
+ foreach($changes as $i => $blocks) {
70
+ // If this is a separate block, we're condensing code so output ...,
71
+ // indicating a significant portion of the code has been collapsed as
72
+ // it is the same
73
+ if($i > 0) {
74
+ $html .= '<tbody class="Skipped">';
75
+ $html .= '<th>&hellip;</th>';
76
+ $html .= '<th>&hellip;</th>';
77
+ $html .= '<td>&nbsp;</td>';
78
+ $html .= '</tbody>';
79
+ }
80
+
81
+ foreach($blocks as $change) {
82
+ $html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
83
+ // Equal changes should be shown on both sides of the diff
84
+ if($change['tag'] == 'equal') {
85
+ foreach($change['base']['lines'] as $no => $line) {
86
+ $fromLine = $change['base']['offset'] + $no + 1;
87
+ $toLine = $change['changed']['offset'] + $no + 1;
88
+ $html .= '<tr>';
89
+ $html .= '<th>'.$fromLine.'</th>';
90
+ $html .= '<th>'.$toLine.'</th>';
91
+ $html .= '<td class="Left">'.$line.'</td>';
92
+ $html .= '</tr>';
93
+ }
94
+ }
95
+ // Added lines only on the right side
96
+ else if($change['tag'] == 'insert') {
97
+ foreach($change['changed']['lines'] as $no => $line) {
98
+ $toLine = $change['changed']['offset'] + $no + 1;
99
+ $html .= '<tr>';
100
+ $html .= '<th>&nbsp;</th>';
101
+ $html .= '<th>'.$toLine.'</th>';
102
+ $html .= '<td class="Right"><ins>'.$line.'</ins>&nbsp;</td>';
103
+ $html .= '</tr>';
104
+ }
105
+ }
106
+ // Show deleted lines only on the left side
107
+ else if($change['tag'] == 'delete') {
108
+ foreach($change['base']['lines'] as $no => $line) {
109
+ $fromLine = $change['base']['offset'] + $no + 1;
110
+ $html .= '<tr>';
111
+ $html .= '<th>'.$fromLine.'</th>';
112
+ $html .= '<th>&nbsp;</th>';
113
+ $html .= '<td class="Left"><del>'.$line.'</del>&nbsp;</td>';
114
+ $html .= '</tr>';
115
+ }
116
+ }
117
+ // Show modified lines on both sides
118
+ else if($change['tag'] == 'replace') {
119
+ foreach($change['base']['lines'] as $no => $line) {
120
+ $fromLine = $change['base']['offset'] + $no + 1;
121
+ $html .= '<tr>';
122
+ $html .= '<th>'.$fromLine.'</th>';
123
+ $html .= '<th>&nbsp;</th>';
124
+ $html .= '<td class="Left"><span>'.$line.'</span></td>';
125
+ $html .= '</tr>';
126
+ }
127
+
128
+ foreach($change['changed']['lines'] as $no => $line) {
129
+ $toLine = $change['changed']['offset'] + $no + 1;
130
+ $html .= '<tr>';
131
+ $html .= '<th>&nbsp;</th>';
132
+ $html .= '<th>'.$toLine.'</th>';
133
+ $html .= '<td class="Right"><span>'.$line.'</span></td>';
134
+ $html .= '</tr>';
135
+ }
136
+ }
137
+ $html .= '</tbody>';
138
+ }
139
+ }
140
+ $html .= '</table>';
141
+ return $html;
142
+ }
143
+ }
libs/diff/Renderer/Html/SideBySide.php ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Side by Side HTML diff generator for PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ require_once dirname(__FILE__).'/Array.php';
44
+
45
+ class Diff_Renderer_Html_SideBySide extends Diff_Renderer_Html_Array
46
+ {
47
+ /**
48
+ * Render a and return diff with changes between the two sequences
49
+ * displayed side by side.
50
+ *
51
+ * @return string The generated side by side diff.
52
+ */
53
+ public function render()
54
+ {
55
+ $changes = parent::render();
56
+
57
+ $html = '';
58
+ if(empty($changes)) {
59
+ return $html;
60
+ }
61
+
62
+ $html .= '<table class="Differences DifferencesSideBySide">';
63
+ //$html .= '<thead>';
64
+ //$html .= '<tr>';
65
+ //$html .= '<th colspan="2">Old Version</th>';
66
+ //$html .= '<th colspan="2">New Version</th>';
67
+ //$html .= '</tr>';
68
+ //$html .= '</thead>';
69
+ foreach($changes as $i => $blocks) {
70
+ if($i > 0) {
71
+ $html .= '<tbody class="Skipped">';
72
+ $html .= '<th>&hellip;</th><td>&nbsp;</td>';
73
+ $html .= '<th>&hellip;</th><td>&nbsp;</td>';
74
+ $html .= '</tbody>';
75
+ }
76
+
77
+ foreach($blocks as $change) {
78
+ $html .= '<tbody class="Change'.ucfirst($change['tag']).'">';
79
+ // Equal changes should be shown on both sides of the diff
80
+ if($change['tag'] == 'equal') {
81
+ foreach($change['base']['lines'] as $no => $line) {
82
+ $fromLine = $change['base']['offset'] + $no + 1;
83
+ $toLine = $change['changed']['offset'] + $no + 1;
84
+ $html .= '<tr>';
85
+ $html .= '<th>'.$fromLine.'</th>';
86
+ $html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</span></td>';
87
+ $html .= '<th>'.$toLine.'</th>';
88
+ $html .= '<td class="Right"><span>'.$line.'</span>&nbsp;</span></td>';
89
+ $html .= '</tr>';
90
+ }
91
+ }
92
+ // Added lines only on the right side
93
+ else if($change['tag'] == 'insert') {
94
+ foreach($change['changed']['lines'] as $no => $line) {
95
+ $toLine = $change['changed']['offset'] + $no + 1;
96
+ $html .= '<tr>';
97
+ $html .= '<th>&nbsp;</th>';
98
+ $html .= '<td class="Left">&nbsp;</td>';
99
+ $html .= '<th>'.$toLine.'</th>';
100
+ $html .= '<td class="Right"><ins>'.$line.'</ins>&nbsp;</td>';
101
+ $html .= '</tr>';
102
+ }
103
+ }
104
+ // Show deleted lines only on the left side
105
+ else if($change['tag'] == 'delete') {
106
+ foreach($change['base']['lines'] as $no => $line) {
107
+ $fromLine = $change['base']['offset'] + $no + 1;
108
+ $html .= '<tr>';
109
+ $html .= '<th>'.$fromLine.'</th>';
110
+ $html .= '<td class="Left"><del>'.$line.'</del>&nbsp;</td>';
111
+ $html .= '<th>&nbsp;</th>';
112
+ $html .= '<td class="Right">&nbsp;</td>';
113
+ $html .= '</tr>';
114
+ }
115
+ }
116
+ // Show modified lines on both sides
117
+ else if($change['tag'] == 'replace') {
118
+ if(count($change['base']['lines']) >= count($change['changed']['lines'])) {
119
+ foreach($change['base']['lines'] as $no => $line) {
120
+ $fromLine = $change['base']['offset'] + $no + 1;
121
+ $html .= '<tr>';
122
+ $html .= '<th>'.$fromLine.'</th>';
123
+ $html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</td>';
124
+ if(!isset($change['changed']['lines'][$no])) {
125
+ $toLine = '&nbsp;';
126
+ $changedLine = '&nbsp;';
127
+ }
128
+ else {
129
+ $toLine = $change['base']['offset'] + $no + 1;
130
+ $changedLine = '<span>'.$change['changed']['lines'][$no].'</span>';
131
+ }
132
+ $html .= '<th>'.$toLine.'</th>';
133
+ $html .= '<td class="Right">'.$changedLine.'</td>';
134
+ $html .= '</tr>';
135
+ }
136
+ }
137
+ else {
138
+ foreach($change['changed']['lines'] as $no => $changedLine) {
139
+ if(!isset($change['base']['lines'][$no])) {
140
+ $fromLine = '&nbsp;';
141
+ $line = '&nbsp;';
142
+ }
143
+ else {
144
+ $fromLine = $change['base']['offset'] + $no + 1;
145
+ $line = '<span>'.$change['base']['lines'][$no].'</span>';
146
+ }
147
+ $html .= '<tr>';
148
+ $html .= '<th>'.$fromLine.'</th>';
149
+ $html .= '<td class="Left"><span>'.$line.'</span>&nbsp;</td>';
150
+ $toLine = $change['changed']['offset'] + $no + 1;
151
+ $html .= '<th>'.$toLine.'</th>';
152
+ $html .= '<td class="Right">'.$changedLine.'</td>';
153
+ $html .= '</tr>';
154
+ }
155
+ }
156
+ }
157
+ $html .= '</tbody>';
158
+ }
159
+ }
160
+ $html .= '</table>';
161
+ return $html;
162
+ }
163
+ }
libs/diff/Renderer/Text/Context.php ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Context diff generator for PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ require_once dirname(__FILE__).'/../Abstract.php';
44
+
45
+ class Diff_Renderer_Text_Context extends Diff_Renderer_Abstract
46
+ {
47
+ /**
48
+ * @var array Array of the different opcode tags and how they map to the context diff equivalent.
49
+ */
50
+ private $tagMap = array(
51
+ 'insert' => '+',
52
+ 'delete' => '-',
53
+ 'replace' => '!',
54
+ 'equal' => ' '
55
+ );
56
+
57
+ /**
58
+ * Render and return a context formatted (old school!) diff file.
59
+ *
60
+ * @return string The generated context diff.
61
+ */
62
+ public function render()
63
+ {
64
+ $diff = '';
65
+ $opCodes = $this->diff->getGroupedOpcodes();
66
+ foreach($opCodes as $group) {
67
+ $diff .= "***************\n";
68
+ $lastItem = count($group)-1;
69
+ $i1 = $group[0][1];
70
+ $i2 = $group[$lastItem][2];
71
+ $j1 = $group[0][3];
72
+ $j2 = $group[$lastItem][4];
73
+
74
+ if($i2 - $i1 >= 2) {
75
+ $diff .= '*** '.($group[0][1] + 1).','.$i2." ****\n";
76
+ }
77
+ else {
78
+ $diff .= '*** '.$i2." ****\n";
79
+ }
80
+
81
+ if($j2 - $j1 >= 2) {
82
+ $separator = '--- '.($j1 + 1).','.$j2." ----\n";
83
+ }
84
+ else {
85
+ $separator = '--- '.$j2." ----\n";
86
+ }
87
+
88
+ $hasVisible = false;
89
+ foreach($group as $code) {
90
+ if($code[0] == 'replace' || $code[0] == 'delete') {
91
+ $hasVisible = true;
92
+ break;
93
+ }
94
+ }
95
+
96
+ if($hasVisible) {
97
+ foreach($group as $code) {
98
+ list($tag, $i1, $i2, $j1, $j2) = $code;
99
+ if($tag == 'insert') {
100
+ continue;
101
+ }
102
+ $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetA($i1, $i2))."\n";
103
+ }
104
+ }
105
+
106
+ $hasVisible = false;
107
+ foreach($group as $code) {
108
+ if($code[0] == 'replace' || $code[0] == 'insert') {
109
+ $hasVisible = true;
110
+ break;
111
+ }
112
+ }
113
+
114
+ $diff .= $separator;
115
+
116
+ if($hasVisible) {
117
+ foreach($group as $code) {
118
+ list($tag, $i1, $i2, $j1, $j2) = $code;
119
+ if($tag == 'delete') {
120
+ continue;
121
+ }
122
+ $diff .= $this->tagMap[$tag].' '.implode("\n".$this->tagMap[$tag].' ', $this->diff->GetB($j1, $j2))."\n";
123
+ }
124
+ }
125
+ }
126
+ return $diff;
127
+ }
128
+ }
libs/diff/Renderer/Text/Unified.php ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Unified diff generator for PHP DiffLib.
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package DiffLib
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ require_once dirname(__FILE__).'/../Abstract.php';
44
+
45
+ class Diff_Renderer_Text_Unified extends Diff_Renderer_Abstract
46
+ {
47
+ /**
48
+ * Render and return a unified diff.
49
+ *
50
+ * @return string The unified diff.
51
+ */
52
+ public function render()
53
+ {
54
+ $diff = '';
55
+ $opCodes = $this->diff->getGroupedOpcodes();
56
+ foreach($opCodes as $group) {
57
+ $lastItem = count($group)-1;
58
+ $i1 = $group[0][1];
59
+ $i2 = $group[$lastItem][2];
60
+ $j1 = $group[0][3];
61
+ $j2 = $group[$lastItem][4];
62
+
63
+ if($i1 == 0 && $i2 == 0) {
64
+ $i1 = -1;
65
+ $i2 = -1;
66
+ }
67
+
68
+ $diff .= '@@ -'.($i1 + 1).','.($i2 - $i1).' +'.($j1 + 1).','.($j2 - $j1)." @@\n";
69
+ foreach($group as $code) {
70
+ list($tag, $i1, $i2, $j1, $j2) = $code;
71
+ if($tag == 'equal') {
72
+ $diff .= ' '.implode("\n ", $this->diff->GetA($i1, $i2))."\n";
73
+ }
74
+ else {
75
+ if($tag == 'replace' || $tag == 'delete') {
76
+ $diff .= '-'.implode("\n-", $this->diff->GetA($i1, $i2))."\n";
77
+ }
78
+
79
+ if($tag == 'replace' || $tag == 'insert') {
80
+ $diff .= '+'.implode("\n+", $this->diff->GetB($j1, $j2))."\n";
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return $diff;
86
+ }
87
+ }
libs/diff/SequenceMatcher.php ADDED
@@ -0,0 +1,742 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Sequence matcher for Diff
4
+ *
5
+ * PHP version 5
6
+ *
7
+ * Copyright (c) 2009 Chris Boulton <chris.boulton@interspire.com>
8
+ *
9
+ * All rights reserved.
10
+ *
11
+ * Redistribution and use in source and binary forms, with or without
12
+ * modification, are permitted provided that the following conditions are met:
13
+ *
14
+ * - Redistributions of source code must retain the above copyright notice,
15
+ * this list of conditions and the following disclaimer.
16
+ * - Redistributions in binary form must reproduce the above copyright notice,
17
+ * this list of conditions and the following disclaimer in the documentation
18
+ * and/or other materials provided with the distribution.
19
+ * - Neither the name of the Chris Boulton nor the names of its contributors
20
+ * may be used to endorse or promote products derived from this software
21
+ * without specific prior written permission.
22
+ *
23
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
27
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
+ * POSSIBILITY OF SUCH DAMAGE.
34
+ *
35
+ * @package Diff
36
+ * @author Chris Boulton <chris.boulton@interspire.com>
37
+ * @copyright (c) 2009 Chris Boulton
38
+ * @license New BSD License http://www.opensource.org/licenses/bsd-license.php
39
+ * @version 1.1
40
+ * @link http://github.com/chrisboulton/php-diff
41
+ */
42
+
43
+ class Diff_SequenceMatcher
44
+ {
45
+ /**
46
+ * @var string|array Either a string or an array containing a callback function to determine if a line is "junk" or not.
47
+ */
48
+ private $junkCallback = null;
49
+
50
+ /**
51
+ * @var array The first sequence to compare against.
52
+ */
53
+ private $a = null;
54
+
55
+ /**
56
+ * @var array The second sequence.
57
+ */
58
+ private $b = null;
59
+
60
+ /**
61
+ * @var array Array of characters that are considered junk from the second sequence. Characters are the array key.
62
+ */
63
+ private $junkDict = array();
64
+
65
+ /**
66
+ * @var array Array of indices that do not contain junk elements.
67
+ */
68
+ private $b2j = array();
69
+
70
+ private $options = array();
71
+
72
+ private $defaultOptions = array(
73
+ 'ignoreNewLines' => false,
74
+ 'ignoreWhitespace' => false,
75
+ 'ignoreCase' => false
76
+ );
77
+
78
+ /**
79
+ * The constructor. With the sequences being passed, they'll be set for the
80
+ * sequence matcher and it will perform a basic cleanup & calculate junk
81
+ * elements.
82
+ *
83
+ * @param string|array $a A string or array containing the lines to compare against.
84
+ * @param string|array $b A string or array containing the lines to compare.
85
+ * @param string|array $junkCallback Either an array or string that references a callback function (if there is one) to determine 'junk' characters.
86
+ */
87
+ public function __construct($a, $b, $junkCallback=null, $options)
88
+ {
89
+ $this->a = null;
90
+ $this->b = null;
91
+ $this->junkCallback = $junkCallback;
92
+ $this->setOptions($options);
93
+ $this->setSequences($a, $b);
94
+ }
95
+
96
+ public function setOptions($options)
97
+ {
98
+ $this->options = array_merge($this->defaultOptions, $options);
99
+ }
100
+
101
+ /**
102
+ * Set the first and second sequences to use with the sequence matcher.
103
+ *
104
+ * @param string|array $a A string or array containing the lines to compare against.
105
+ * @param string|array $b A string or array containing the lines to compare.
106
+ */
107
+ public function setSequences($a, $b)
108
+ {
109
+ $this->setSeq1($a);
110
+ $this->setSeq2($b);
111
+ }
112
+
113
+ /**
114
+ * Set the first sequence ($a) and reset any internal caches to indicate that
115
+ * when calling the calculation methods, we need to recalculate them.
116
+ *
117
+ * @param string|array $a The sequence to set as the first sequence.
118
+ */
119
+ public function setSeq1($a)
120
+ {
121
+ if(!is_array($a)) {
122
+ $a = str_split($a);
123
+ }
124
+ if($a == $this->a) {
125
+ return;
126
+ }
127
+
128
+ $this->a= $a;
129
+ $this->matchingBlocks = null;
130
+ $this->opCodes = null;
131
+ }
132
+
133
+ /**
134
+ * Set the second sequence ($b) and reset any internal caches to indicate that
135
+ * when calling the calculation methods, we need to recalculate them.
136
+ *
137
+ * @param string|array $b The sequence to set as the second sequence.
138
+ */
139
+ public function setSeq2($b)
140
+ {
141
+ if(!is_array($b)) {
142
+ $b = str_split($b);
143
+ }
144
+ if($b == $this->b) {
145
+ return;
146
+ }
147
+
148
+ $this->b = $b;
149
+ $this->matchingBlocks = null;
150
+ $this->opCodes = null;
151
+ $this->fullBCount = null;
152
+ $this->chainB();
153
+ }
154
+
155
+ /**
156
+ * Generate the internal arrays containing the list of junk and non-junk
157
+ * characters for the second ($b) sequence.
158
+ */
159
+ private function chainB()
160
+ {
161
+ $length = count ($this->b);
162
+ $this->b2j = array();
163
+ $popularDict = array();
164
+
165
+ for($i = 0; $i < $length; ++$i) {
166
+ $char = $this->b[$i];
167
+ if(isset($this->b2j[$char])) {
168
+ if($length >= 200 && count($this->b2j[$char]) * 100 > $length) {
169
+ $popularDict[$char] = 1;
170
+ unset($this->b2j[$char]);
171
+ }
172
+ else {
173
+ $this->b2j[$char][] = $i;
174
+ }
175
+ }
176
+ else {
177
+ $this->b2j[$char] = array(
178
+ $i
179
+ );
180
+ }
181
+ }
182
+
183
+ // Remove leftovers
184
+ foreach(array_keys($popularDict) as $char) {
185
+ unset($this->b2j[$char]);
186
+ }
187
+
188
+ $this->junkDict = array();
189
+ if(is_callable($this->junkCallback)) {
190
+ foreach(array_keys($popularDict) as $char) {
191
+ if(call_user_func($this->junkCallback, $char)) {
192
+ $this->junkDict[$char] = 1;
193
+ unset($popularDict[$char]);
194
+ }
195
+ }
196
+
197
+ foreach(array_keys($this->b2j) as $char) {
198
+ if(call_user_func($this->junkCallback, $char)) {
199
+ $this->junkDict[$char] = 1;
200
+ unset($this->b2j[$char]);
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Checks if a particular character is in the junk dictionary
208
+ * for the list of junk characters.
209
+ *
210
+ * @return boolean $b True if the character is considered junk. False if not.
211
+ */
212
+ private function isBJunk($b)
213
+ {
214
+ if(isset($this->juncDict[$b])) {
215
+ return true;
216
+ }
217
+
218
+ return false;
219
+ }
220
+
221
+ /**
222
+ * Find the longest matching block in the two sequences, as defined by the
223
+ * lower and upper constraints for each sequence. (for the first sequence,
224
+ * $alo - $ahi and for the second sequence, $blo - $bhi)
225
+ *
226
+ * Essentially, of all of the maximal matching blocks, return the one that
227
+ * startest earliest in $a, and all of those maximal matching blocks that
228
+ * start earliest in $a, return the one that starts earliest in $b.
229
+ *
230
+ * If the junk callback is defined, do the above but with the restriction
231
+ * that the junk element appears in the block. Extend it as far as possible
232
+ * by matching only junk elements in both $a and $b.
233
+ *
234
+ * @param int $alo The lower constraint for the first sequence.
235
+ * @param int $ahi The upper constraint for the first sequence.
236
+ * @param int $blo The lower constraint for the second sequence.
237
+ * @param int $bhi The upper constraint for the second sequence.
238
+ * @return array Array containing the longest match that includes the starting position in $a, start in $b and the length/size.
239
+ */
240
+ public function findLongestMatch($alo, $ahi, $blo, $bhi)
241
+ {
242
+ $a = $this->a;
243
+ $b = $this->b;
244
+
245
+ $bestI = $alo;
246
+ $bestJ = $blo;
247
+ $bestSize = 0;
248
+
249
+ $j2Len = array();
250
+ $nothing = array();
251
+
252
+ for($i = $alo; $i < $ahi; ++$i) {
253
+ $newJ2Len = array();
254
+ $jDict = $this->arrayGetDefault($this->b2j, $a[$i], $nothing);
255
+ foreach($jDict as $jKey => $j) {
256
+ if($j < $blo) {
257
+ continue;
258
+ }
259
+ else if($j >= $bhi) {
260
+ break;
261
+ }
262
+
263
+ $k = $this->arrayGetDefault($j2Len, $j -1, 0) + 1;
264
+ $newJ2Len[$j] = $k;
265
+ if($k > $bestSize) {
266
+ $bestI = $i - $k + 1;
267
+ $bestJ = $j - $k + 1;
268
+ $bestSize = $k;
269
+ }
270
+ }
271
+
272
+ $j2Len = $newJ2Len;
273
+ }
274
+
275
+ while($bestI > $alo && $bestJ > $blo && !$this->isBJunk($b[$bestJ - 1]) &&
276
+ !$this->linesAreDifferent($bestI - 1, $bestJ - 1)) {
277
+ --$bestI;
278
+ --$bestJ;
279
+ ++$bestSize;
280
+ }
281
+
282
+ while($bestI + $bestSize < $ahi && ($bestJ + $bestSize) < $bhi &&
283
+ !$this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
284
+ ++$bestSize;
285
+ }
286
+
287
+ while($bestI > $alo && $bestJ > $blo && $this->isBJunk($b[$bestJ - 1]) &&
288
+ !$this->isLineDifferent($bestI - 1, $bestJ - 1)) {
289
+ --$bestI;
290
+ --$bestJ;
291
+ ++$bestSize;
292
+ }
293
+
294
+ while($bestI + $bestSize < $ahi && $bestJ + $bestSize < $bhi &&
295
+ $this->isBJunk($b[$bestJ + $bestSize]) && !$this->linesAreDifferent($bestI + $bestSize, $bestJ + $bestSize)) {
296
+ ++$bestSize;
297
+ }
298
+
299
+ return array(
300
+ $bestI,
301
+ $bestJ,
302
+ $bestSize
303
+ );
304
+ }
305
+
306
+ /**
307
+ * Check if the two lines at the given indexes are different or not.
308
+ *
309
+ * @param int $aIndex Line number to check against in a.
310
+ * @param int $bIndex Line number to check against in b.
311
+ * @return boolean True if the lines are different and false if not.
312
+ */
313
+ public function linesAreDifferent($aIndex, $bIndex)
314
+ {
315
+ $lineA = $this->a[$aIndex];
316
+ $lineB = $this->b[$bIndex];
317
+
318
+ if($this->options['ignoreWhitespace']) {
319
+ $replace = array("\t", ' ');
320
+ $lineA = str_replace($replace, '', $lineA);
321
+ $lineB = str_replace($replace, '', $lineB);
322
+ }
323
+
324
+ if($this->options['ignoreCase']) {
325
+ $lineA = strtolower($lineA);
326
+ $lineB = strtolower($lineB);
327
+ }
328
+
329
+ if($lineA != $lineB) {
330
+ return true;
331
+ }
332
+
333
+ return false;
334
+ }
335
+
336
+ /**
337
+ * Return a nested set of arrays for all of the matching sub-sequences
338
+ * in the strings $a and $b.
339
+ *
340
+ * Each block contains the lower constraint of the block in $a, the lower
341
+ * constraint of the block in $b and finally the number of lines that the
342
+ * block continues for.
343
+ *
344
+ * @return array Nested array of the matching blocks, as described by the function.
345
+ */
346
+ public function getMatchingBlocks()
347
+ {
348
+ if(!empty($this->matchingBlocks)) {
349
+ return $this->matchingBlocks;
350
+ }
351
+
352
+ $aLength = count($this->a);
353
+ $bLength = count($this->b);
354
+
355
+ $queue = array(
356
+ array(
357
+ 0,
358
+ $aLength,
359
+ 0,
360
+ $bLength
361
+ )
362
+ );
363
+
364
+ $matchingBlocks = array();
365
+ while(!empty($queue)) {
366
+ list($alo, $ahi, $blo, $bhi) = array_pop($queue);
367
+ $x = $this->findLongestMatch($alo, $ahi, $blo, $bhi);
368
+ list($i, $j, $k) = $x;
369
+ if($k) {
370
+ $matchingBlocks[] = $x;
371
+ if($alo < $i && $blo < $j) {
372
+ $queue[] = array(
373
+ $alo,
374
+ $i,
375
+ $blo,
376
+ $j
377
+ );
378
+ }
379
+
380
+ if($i + $k < $ahi && $j + $k < $bhi) {
381
+ $queue[] = array(
382
+ $i + $k,
383
+ $ahi,
384
+ $j + $k,
385
+ $bhi
386
+ );
387
+ }
388
+ }
389
+ }
390
+
391
+ usort($matchingBlocks, array($this, 'tupleSort'));
392
+
393
+ $i1 = 0;
394
+ $j1 = 0;
395
+ $k1 = 0;
396
+ $nonAdjacent = array();
397
+ foreach($matchingBlocks as $block) {
398
+ list($i2, $j2, $k2) = $block;
399
+ if($i1 + $k1 == $i2 && $j1 + $k1 == $j2) {
400
+ $k1 += $k2;
401
+ }
402
+ else {
403
+ if($k1) {
404
+ $nonAdjacent[] = array(
405
+ $i1,
406
+ $j1,
407
+ $k1
408
+ );
409
+ }
410
+
411
+ $i1 = $i2;
412
+ $j1 = $j2;
413
+ $k1 = $k2;
414
+ }
415
+ }
416
+
417
+ if($k1) {
418
+ $nonAdjacent[] = array(
419
+ $i1,
420
+ $j1,
421
+ $k1
422
+ );
423
+ }
424
+
425
+ $nonAdjacent[] = array(
426
+ $aLength,
427
+ $bLength,
428
+ 0
429
+ );
430
+
431
+ $this->matchingBlocks = $nonAdjacent;
432
+ return $this->matchingBlocks;
433
+ }
434
+
435
+ /**
436
+ * Return a list of all of the opcodes for the differences between the
437
+ * two strings.
438
+ *
439
+ * The nested array returned contains an array describing the opcode
440
+ * which includes:
441
+ * 0 - The type of tag (as described below) for the opcode.
442
+ * 1 - The beginning line in the first sequence.
443
+ * 2 - The end line in the first sequence.
444
+ * 3 - The beginning line in the second sequence.
445
+ * 4 - The end line in the second sequence.
446
+ *
447
+ * The different types of tags include:
448
+ * replace - The string from $i1 to $i2 in $a should be replaced by
449
+ * the string in $b from $j1 to $j2.
450
+ * delete - The string in $a from $i1 to $j2 should be deleted.
451
+ * insert - The string in $b from $j1 to $j2 should be inserted at
452
+ * $i1 in $a.
453
+ * equal - The two strings with the specified ranges are equal.
454
+ *
455
+ * @return array Array of the opcodes describing the differences between the strings.
456
+ */
457
+ public function getOpCodes()
458
+ {
459
+ if(!empty($this->opCodes)) {
460
+ return $this->opCodes;
461
+ }
462
+
463
+ $i = 0;
464
+ $j = 0;
465
+ $this->opCodes = array();
466
+
467
+ $blocks = $this->getMatchingBlocks();
468
+ foreach($blocks as $block) {
469
+ list($ai, $bj, $size) = $block;
470
+ $tag = '';
471
+ if($i < $ai && $j < $bj) {
472
+ $tag = 'replace';
473
+ }
474
+ else if($i < $ai) {
475
+ $tag = 'delete';
476
+ }
477
+ else if($j < $bj) {
478
+ $tag = 'insert';
479
+ }
480
+
481
+ if($tag) {
482
+ $this->opCodes[] = array(
483
+ $tag,
484
+ $i,
485
+ $ai,
486
+ $j,
487
+ $bj
488
+ );
489
+ }
490
+
491
+ $i = $ai + $size;
492
+ $j = $bj + $size;
493
+
494
+ if($size) {
495
+ $this->opCodes[] = array(
496
+ 'equal',
497
+ $ai,
498
+ $i,
499
+ $bj,
500
+ $j
501
+ );
502
+ }
503
+ }
504
+ return $this->opCodes;
505
+ }
506
+
507
+ /**
508
+ * Return a series of nested arrays containing different groups of generated
509
+ * opcodes for the differences between the strings with up to $context lines
510
+ * of surrounding content.
511
+ *
512
+ * Essentially what happens here is any big equal blocks of strings are stripped
513
+ * out, the smaller subsets of changes are then arranged in to their groups.
514
+ * This means that the sequence matcher and diffs do not need to include the full
515
+ * content of the different files but can still provide context as to where the
516
+ * changes are.
517
+ *
518
+ * @param int $context The number of lines of context to provide around the groups.
519
+ * @return array Nested array of all of the grouped opcodes.
520
+ */
521
+ public function getGroupedOpcodes($context=3)
522
+ {
523
+ $opCodes = $this->getOpCodes();
524
+ if(empty($opCodes)) {
525
+ $opCodes = array(
526
+ array(
527
+ 'equal',
528
+ 0,
529
+ 1,
530
+ 0,
531
+ 1
532
+ )
533
+ );
534
+ }
535
+
536
+ if($opCodes[0][0] == 'equal') {
537
+ $opCodes[0] = array(
538
+ $opCodes[0][0],
539
+ max($opCodes[0][1], $opCodes[0][2] - $context),
540
+ $opCodes[0][2],
541
+ max($opCodes[0][3], $opCodes[0][4] - $context),
542
+ $opCodes[0][4]
543
+ );
544
+ }
545
+
546
+ $lastItem = count($opCodes) - 1;
547
+ if($opCodes[$lastItem][0] == 'equal') {
548
+ list($tag, $i1, $i2, $j1, $j2) = $opCodes[$lastItem];
549
+ $opCodes[$lastItem] = array(
550
+ $tag,
551
+ $i1,
552
+ min($i2, $i1 + $context),
553
+ $j1,
554
+ min($j2, $j1 + $context)
555
+ );
556
+ }
557
+
558
+ $maxRange = $context * 2;
559
+ $groups = array();
560
+ $group = array();
561
+ foreach($opCodes as $code) {
562
+ list($tag, $i1, $i2, $j1, $j2) = $code;
563
+ if($tag == 'equal' && $i2 - $i1 > $maxRange) {
564
+ $group[] = array(
565
+ $tag,
566
+ $i1,
567
+ min($i2, $i1 + $context),
568
+ $j1,
569
+ min($j2, $j1 + $context)
570
+ );
571
+ $groups[] = $group;
572
+ $group = array();
573
+ $i1 = max($i1, $i2 - $context);
574
+ $j1 = max($j1, $j2 - $context);
575
+ }
576
+ $group[] = array(
577
+ $tag,
578
+ $i1,
579
+ $i2,
580
+ $j1,
581
+ $j2
582
+ );
583
+ }
584
+
585
+ if(!empty($group) && !(count($group) == 1 && $group[0][0] == 'equal')) {
586
+ $groups[] = $group;
587
+ }
588
+
589
+ return $groups;
590
+ }
591
+
592
+ /**
593
+ * Return a measure of the similarity between the two sequences.
594
+ * This will be a float value between 0 and 1.
595
+ *
596
+ * Out of all of the ratio calculation functions, this is the most
597
+ * expensive to call if getMatchingBlocks or getOpCodes is yet to be
598
+ * called. The other calculation methods (quickRatio and realquickRatio)
599
+ * can be used to perform quicker calculations but may be less accurate.
600
+ *
601
+ * The ratio is calculated as (2 * number of matches) / total number of
602
+ * elements in both sequences.
603
+ *
604
+ * @return float The calculated ratio.
605
+ */
606
+ public function Ratio()
607
+ {
608
+ $matches = array_reduce($this->getMatchingBlocks(), array($this, 'ratioReduce'), 0);
609
+ return $this->calculateRatio($matches, count ($this->a) + count ($this->b));
610
+ }
611
+
612
+ /**
613
+ * Helper function to calculate the number of matches for Ratio().
614
+ *
615
+ * @param int $sum The running total for the number of matches.
616
+ * @param array $triple Array containing the matching block triple to add to the running total.
617
+ * @return int The new running total for the number of matches.
618
+ */
619
+ private function ratioReduce($sum, $triple)
620
+ {
621
+ return $sum + ($triple[count($triple) - 1]);
622
+ }
623
+
624
+ /**
625
+ * Quickly return an upper bound ratio for the similarity of the strings.
626
+ * This is quicker to compute than Ratio().
627
+ *
628
+ * @return float The calculated ratio.
629
+ */
630
+ private function quickRatio()
631
+ {
632
+ if($this->fullBCount === null) {
633
+ $this->fullBCount = array();
634
+ $bLength = count ($b);
635
+ for($i = 0; $i < $bLength; ++$i) {
636
+ $char = $this->b[$i];
637
+ $this->fullBCount[$char] = $this->arrayGetDefault($this->fullBCount, $char, 0) + 1;
638
+ }
639
+ }
640
+
641
+ $avail = array();
642
+ $matches = 0;
643
+ $aLength = count ($this->a);
644
+ for($i = 0; $i < $aLength; ++$i) {
645
+ $char = $this->a[$i];
646
+ if(isset($avail[$char])) {
647
+ $numb = $avail[$char];
648
+ }
649
+ else {
650
+ $numb = $this->arrayGetDefault($this->fullBCount, $char, 0);
651
+ }
652
+ $avail[$char] = $numb - 1;
653
+ if($numb > 0) {
654
+ ++$matches;
655
+ }
656
+ }
657
+
658
+ $this->calculateRatio($matches, count ($this->a) + count ($this->b));
659
+ }
660
+
661
+ /**
662
+ * Return an upper bound ratio really quickly for the similarity of the strings.
663
+ * This is quicker to compute than Ratio() and quickRatio().
664
+ *
665
+ * @return float The calculated ratio.
666
+ */
667
+ private function realquickRatio()
668
+ {
669
+ $aLength = count ($this->a);
670
+ $bLength = count ($this->b);
671
+
672
+ return $this->calculateRatio(min($aLength, $bLength), $aLength + $bLength);
673
+ }
674
+
675
+ /**
676
+ * Helper function for calculating the ratio to measure similarity for the strings.
677
+ * The ratio is defined as being 2 * (number of matches / total length)
678
+ *
679
+ * @param int $matches The number of matches in the two strings.
680
+ * @param int $length The length of the two strings.
681
+ * @return float The calculated ratio.
682
+ */
683
+ private function calculateRatio($matches, $length=0)
684
+ {
685
+ if($length) {
686
+ return 2 * ($matches / $length);
687
+ }
688
+ else {
689
+ return 1;
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Helper function that provides the ability to return the value for a key
695
+ * in an array of it exists, or if it doesn't then return a default value.
696
+ * Essentially cleaner than doing a series of if(isset()) {} else {} calls.
697
+ *
698
+ * @param array $array The array to search.
699
+ * @param string $key The key to check that exists.
700
+ * @param mixed $default The value to return as the default value if the key doesn't exist.
701
+ * @return mixed The value from the array if the key exists or otherwise the default.
702
+ */
703
+ private function arrayGetDefault($array, $key, $default)
704
+ {
705
+ if(isset($array[$key])) {
706
+ return $array[$key];
707
+ }
708
+ else {
709
+ return $default;
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Sort an array by the nested arrays it contains. Helper function for getMatchingBlocks
715
+ *
716
+ * @param array $a First array to compare.
717
+ * @param array $b Second array to compare.
718
+ * @return int -1, 0 or 1, as expected by the usort function.
719
+ */
720
+ private function tupleSort($a, $b)
721
+ {
722
+ $max = max(count($a), count($b));
723
+ for($i = 0; $i < $max; ++$i) {
724
+ if($a[$i] < $b[$i]) {
725
+ return -1;
726
+ }
727
+ else if($a[$i] > $b[$i]) {
728
+ return 1;
729
+ }
730
+ }
731
+
732
+ if(count($a) == $count($b)) {
733
+ return 0;
734
+ }
735
+ else if(count($a) < count($b)) {
736
+ return -1;
737
+ }
738
+ else {
739
+ return 1;
740
+ }
741
+ }
742
+ }
libs/dumper.php ADDED
@@ -0,0 +1,514 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Abstract dump file: provides common interface for writing
4
+ * data to dump files.
5
+ * (c) 2create Studio, Bulgaria
6
+ * http://2create.bg/
7
+ */
8
+ abstract class Shuttle_Dump_File {
9
+ /**
10
+ * File Handle
11
+ */
12
+ protected $fh;
13
+
14
+ /**
15
+ * Location of the dump file on the disk
16
+ */
17
+ protected $file_location;
18
+
19
+ abstract function write($string);
20
+ abstract function end();
21
+
22
+ static function create($filename) {
23
+ if (self::is_gzip($filename)) {
24
+ return new Shuttle_Dump_File_Gzip($filename);
25
+ }
26
+ return new Shuttle_Dump_File_Plaintext($filename);
27
+ }
28
+ function __construct($file) {
29
+ $this->file_location = $file;
30
+ $this->fh = $this->open();
31
+
32
+ if (!$this->fh) {
33
+ throw new Shuttle_Exception("Couldn't create gz file");
34
+ }
35
+ }
36
+
37
+ public static function is_gzip($filename) {
38
+ return preg_match('~gz$~i', $filename);
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Plain text implementation. Uses standard file functions in PHP.
44
+ */
45
+ class Shuttle_Dump_File_Plaintext extends Shuttle_Dump_File {
46
+ function open() {
47
+ return fopen($this->file_location, 'w');
48
+ }
49
+ function write($string) {
50
+ return fwrite($this->fh, $string);
51
+ }
52
+ function end() {
53
+ return fclose($this->fh);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Gzip implementation. Uses gz* functions.
59
+ */
60
+ class Shuttle_Dump_File_Gzip extends Shuttle_Dump_File {
61
+ function open() {
62
+ return gzopen($this->file_location, 'wb9');
63
+ }
64
+ function write($string) {
65
+ return gzwrite($this->fh, $string);
66
+ }
67
+ function end() {
68
+ return gzclose($this->fh);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * MySQL insert statement builder.
74
+ */
75
+ class Shuttle_Insert_Statement {
76
+ private $rows = array();
77
+ private $length = 0;
78
+ private $table;
79
+
80
+ function __construct($table) {
81
+ $this->table = $table;
82
+ }
83
+
84
+ function reset() {
85
+ $this->rows = array();
86
+ $this->length = 0;
87
+ }
88
+
89
+ function add_row($row) {
90
+ $row = '(' . implode(",", $row) . ')';
91
+ $this->rows[] = $row;
92
+ $this->length += strlen($row);
93
+ }
94
+
95
+ function get_sql() {
96
+ if (empty($this->rows)) {
97
+ return false;
98
+ }
99
+
100
+ return 'INSERT INTO `' . $this->table . '` VALUES ' .
101
+ implode(",\n", $this->rows) . '; ';
102
+ }
103
+
104
+ function get_length() {
105
+ return $this->length;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Main facade
111
+ */
112
+ abstract class Shuttle_Dumper {
113
+ /**
114
+ * Maximum length of single insert statement
115
+ */
116
+ const INSERT_THRESHOLD = 838860;
117
+
118
+ /**
119
+ * @var Shuttle_DBConn
120
+ */
121
+ public $db;
122
+
123
+ /**
124
+ * @var Shuttle_Dump_File
125
+ */
126
+ public $dump_file;
127
+
128
+ /**
129
+ * End of line style used in the dump
130
+ */
131
+ public $eol = "\r\n";
132
+
133
+ /**
134
+ * Specificed tables to include
135
+ */
136
+ public $include_tables;
137
+
138
+ /**
139
+ * Specified tables to exclude
140
+ */
141
+ public $exclude_tables = array();
142
+
143
+ /**
144
+ * Factory method for dumper on current hosts's configuration.
145
+ */
146
+ static function create($db_options) {
147
+ $db = Shuttle_DBConn::create($db_options);
148
+
149
+ $db->connect();
150
+
151
+ if (self::has_shell_access()
152
+ && self::is_shell_command_available('mysqldump')
153
+ && self::is_shell_command_available('gzip')
154
+ ) {
155
+ $dumper = new Shuttle_Dumper_ShellCommand($db);
156
+ } else {
157
+ $dumper = new Shuttle_Dumper_Native($db);
158
+ }
159
+
160
+ if (isset($db_options['include_tables'])) {
161
+ $dumper->include_tables = $db_options['include_tables'];
162
+ }
163
+ if (isset($db_options['exclude_tables'])) {
164
+ $dumper->exclude_tables = $db_options['exclude_tables'];
165
+ }
166
+
167
+ return $dumper;
168
+ }
169
+
170
+ function __construct(Shuttle_DBConn $db) {
171
+ $this->db = $db;
172
+ }
173
+
174
+ public static function has_shell_access() {
175
+ if (!is_callable('shell_exec')) {
176
+ return false;
177
+ }
178
+ $disabled_functions = ini_get('disable_functions');
179
+ return stripos($disabled_functions, 'shell_exec') === false;
180
+ }
181
+
182
+ public static function is_shell_command_available($command) {
183
+ if (preg_match('~win~i', PHP_OS)) {
184
+ /*
185
+ On Windows, the `where` command checks for availabilty in PATH. According
186
+ to the manual(`where /?`), there is quiet mode:
187
+ ....
188
+ /Q Returns only the exit code, without displaying the list
189
+ of matched files. (Quiet mode)
190
+ ....
191
+ */
192
+ $output = array();
193
+ exec('where /Q ' . $command, $output, $return_val);
194
+
195
+ if (intval($return_val) === 1) {
196
+ return false;
197
+ } else {
198
+ return true;
199
+ }
200
+
201
+ } else {
202
+ $last_line = exec('which ' . $command);
203
+ $last_line = trim($last_line);
204
+
205
+ // Whenever there is at least one line in the output,
206
+ // it should be the path to the executable
207
+ if (empty($last_line)) {
208
+ return false;
209
+ } else {
210
+ return true;
211
+ }
212
+ }
213
+
214
+ }
215
+
216
+ /**
217
+ * Create an export file from the tables with that prefix.
218
+ * @param string $export_file_location the file to put the dump to.
219
+ * Note that whenever the file has .gz extension the dump will be comporessed with gzip
220
+ * @param string $table_prefix Allow to export only tables with particular prefix
221
+ * @return void
222
+ */
223
+ abstract public function dump($export_file_location, $table_prefix='');
224
+
225
+ protected function get_tables($table_prefix) {
226
+ if (!empty($this->include_tables)) {
227
+ return $this->include_tables;
228
+ }
229
+
230
+ // $tables will only include the tables and not views.
231
+ // TODO - Handle views also, edits to be made in function 'get_create_table_sql' line 336
232
+ $tables = $this->db->fetch_numeric('
233
+ SHOW FULL TABLES WHERE Table_Type = "BASE TABLE" AND Tables_in_'.$this->db->name.' LIKE "' . $this->db->escape_like($table_prefix) . '%"
234
+ ');
235
+
236
+ $tables_list = array();
237
+ foreach ($tables as $table_row) {
238
+ $table_name = $table_row[0];
239
+ if (!in_array($table_name, $this->exclude_tables)) {
240
+ $tables_list[] = $table_name;
241
+ }
242
+ }
243
+ return $tables_list;
244
+ }
245
+ }
246
+
247
+ class Shuttle_Dumper_ShellCommand extends Shuttle_Dumper {
248
+ function dump($export_file_location, $table_prefix='') {
249
+ $command = 'mysqldump -h ' . escapeshellarg($this->db->host) .
250
+ ' -u ' . escapeshellarg($this->db->username) .
251
+ ' --password=' . escapeshellarg($this->db->password) .
252
+ ' ' . escapeshellarg($this->db->name);
253
+
254
+ $include_all_tables = empty($table_prefix) &&
255
+ empty($this->include_tables) &&
256
+ empty($this->exclude_tables);
257
+
258
+ if (!$include_all_tables) {
259
+ $tables = $this->get_tables($table_prefix);
260
+ $command .= ' ' . implode(' ', array_map('escapeshellarg', $tables));
261
+ }
262
+
263
+ $error_file = tempnam(sys_get_temp_dir(), 'err');
264
+
265
+ $command .= ' 2> ' . escapeshellarg($error_file);
266
+
267
+ if (Shuttle_Dump_File::is_gzip($export_file_location)) {
268
+ $command .= ' | gzip';
269
+ }
270
+
271
+ $command .= ' > ' . escapeshellarg($export_file_location);
272
+
273
+ exec($command, $output, $return_val);
274
+
275
+ if ($return_val !== 0) {
276
+ $error_text = file_get_contents($error_file);
277
+ unlink($error_file);
278
+ throw new Shuttle_Exception('Couldn\'t export database: ' . $error_text);
279
+ }
280
+
281
+ unlink($error_file);
282
+ }
283
+ }
284
+
285
+ class Shuttle_Dumper_Native extends Shuttle_Dumper {
286
+ public function dump($export_file_location, $table_prefix='') {
287
+ $eol = $this->eol;
288
+
289
+ $this->dump_file = Shuttle_Dump_File::create($export_file_location);
290
+
291
+ $this->dump_file->write("-- Generation time: " . date('r') . $eol);
292
+ $this->dump_file->write("-- Host: " . $this->db->host . $eol);
293
+ $this->dump_file->write("-- DB name: " . $this->db->name . $eol);
294
+ $this->dump_file->write("/*!40030 SET NAMES UTF8 */;$eol");
295
+
296
+ $this->dump_file->write("/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;$eol");
297
+ $this->dump_file->write("/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;$eol");
298
+ $this->dump_file->write("/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;$eol");
299
+ $this->dump_file->write("/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;$eol");
300
+ $this->dump_file->write("/*!40103 SET TIME_ZONE='+00:00' */;$eol");
301
+ $this->dump_file->write("/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;$eol");
302
+ $this->dump_file->write("/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;$eol");
303
+ $this->dump_file->write("/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;$eol");
304
+ $this->dump_file->write("/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;$eol$eol");
305
+
306
+
307
+ $tables = $this->get_tables($table_prefix);
308
+ foreach ($tables as $table) {
309
+ $this->dump_table($table);
310
+ }
311
+
312
+ $this->dump_file->write("$eol$eol");
313
+ $this->dump_file->write("/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;$eol");
314
+ $this->dump_file->write("/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;$eol");
315
+ $this->dump_file->write("/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;$eol");
316
+ $this->dump_file->write("/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;$eol");
317
+ $this->dump_file->write("/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;$eol");
318
+ $this->dump_file->write("/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;$eol");
319
+ $this->dump_file->write("/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;$eol$eol");
320
+
321
+ unset($this->dump_file);
322
+ }
323
+
324
+ protected function dump_table($table) {
325
+ $eol = $this->eol;
326
+
327
+ $this->dump_file->write("DROP TABLE IF EXISTS `$table`;$eol");
328
+
329
+ $create_table_sql = $this->get_create_table_sql($table);
330
+ $this->dump_file->write($create_table_sql . $eol . $eol);
331
+
332
+ $data = $this->db->query("SELECT * FROM `$table`");
333
+
334
+ $insert = new Shuttle_Insert_Statement($table);
335
+
336
+ while ($row = $this->db->fetch_row($data)) {
337
+ $row_values = array();
338
+ foreach ($row as $value) {
339
+ $row_values[] = $this->db->escape($value);
340
+ }
341
+ $insert->add_row( $row_values );
342
+
343
+ if ($insert->get_length() > self::INSERT_THRESHOLD) {
344
+ // The insert got too big: write the SQL and create
345
+ // new insert statement
346
+ $this->dump_file->write($insert->get_sql() . $eol);
347
+ $insert->reset();
348
+ }
349
+ }
350
+
351
+ $sql = $insert->get_sql();
352
+ if ($sql) {
353
+ $this->dump_file->write($insert->get_sql() . $eol);
354
+ }
355
+ $this->dump_file->write($eol . $eol);
356
+ }
357
+
358
+ public function get_create_table_sql($table) {
359
+ $create_table_sql = $this->db->fetch('SHOW CREATE TABLE `' . $table . '`');
360
+ return $create_table_sql[0]['Create Table'] . ';';
361
+ }
362
+ }
363
+
364
+ class Shuttle_DBConn {
365
+ public $host;
366
+ public $username;
367
+ public $password;
368
+ public $name;
369
+
370
+ protected $connection;
371
+
372
+ function __construct($options) {
373
+ $this->host = $options['host'];
374
+ if (empty($this->host)) {
375
+ $this->host = '127.0.0.1';
376
+ }
377
+ $this->username = $options['username'];
378
+ $this->password = $options['password'];
379
+ $this->name = $options['db_name'];
380
+ }
381
+
382
+ static function create($options) {
383
+ if (class_exists('mysqli')) {
384
+ $class_name = "Shuttle_DBConn_Mysqli";
385
+ } else {
386
+ $class_name = "Shuttle_DBConn_Mysql";
387
+ }
388
+
389
+ return new $class_name($options);
390
+ }
391
+ }
392
+
393
+ class Shuttle_DBConn_Mysql extends Shuttle_DBConn {
394
+ function connect() {
395
+ $this->connection = @mysql_connect($this->host, $this->username, $this->password);
396
+ if (!$this->connection) {
397
+ throw new Shuttle_Exception("Couldn't connect to the database: " . mysql_error());
398
+ }
399
+
400
+ $select_db_res = mysql_select_db($this->name, $this->connection);
401
+ if (!$select_db_res) {
402
+ throw new Shuttle_Exception("Couldn't select database: " . mysql_error($this->connection));
403
+ }
404
+
405
+ return true;
406
+ }
407
+
408
+ function query($q) {
409
+ if (!$this->connection) {
410
+ $this->connect();
411
+ }
412
+ $res = mysql_query($q);
413
+ if (!$res) {
414
+ throw new Shuttle_Exception("SQL error: " . mysql_error($this->connection));
415
+ }
416
+ return $res;
417
+ }
418
+
419
+ function fetch_numeric($query) {
420
+ return $this->fetch($query, MYSQL_NUM);
421
+ }
422
+
423
+ function fetch($query, $result_type=MYSQL_ASSOC) {
424
+ $result = $this->query($query, $this->connection);
425
+ $return = array();
426
+ while ( $row = mysql_fetch_array($result, $result_type) ) {
427
+ $return[] = $row;
428
+ }
429
+ return $return;
430
+ }
431
+
432
+ function escape($value) {
433
+ if (is_null($value)) {
434
+ return "NULL";
435
+ }
436
+ return "'" . mysql_real_escape_string($value) . "'";
437
+ }
438
+
439
+ function escape_like($search) {
440
+ return str_replace(array('_', '%'), array('\_', '\%'), $search);
441
+ }
442
+
443
+ function get_var($sql) {
444
+ $result = $this->query($sql);
445
+ $row = mysql_fetch_array($result);
446
+ return $row[0];
447
+ }
448
+
449
+ function fetch_row($data) {
450
+ return mysql_fetch_assoc($data);
451
+ }
452
+ }
453
+
454
+
455
+ class Shuttle_DBConn_Mysqli extends Shuttle_DBConn {
456
+ function connect() {
457
+ $this->connection = @new MySQLi($this->host, $this->username, $this->password, $this->name);
458
+
459
+ if ($this->connection->connect_error) {
460
+ throw new Shuttle_Exception("Couldn't connect to the database: " . $this->connection->connect_error);
461
+ }
462
+
463
+ return true;
464
+ }
465
+
466
+ function query($q) {
467
+ if (!$this->connection) {
468
+ $this->connect();
469
+ }
470
+ $res = $this->connection->query($q);
471
+
472
+ if (!$res) {
473
+ throw new Shuttle_Exception("SQL error: " . $this->connection->error);
474
+ }
475
+
476
+ return $res;
477
+ }
478
+
479
+ function fetch_numeric($query) {
480
+ return $this->fetch($query, MYSQLI_NUM);
481
+ }
482
+
483
+ function fetch($query, $result_type=MYSQLI_ASSOC) {
484
+ $result = $this->query($query, $this->connection);
485
+ $return = array();
486
+ while ( $row = $result->fetch_array($result_type) ) {
487
+ $return[] = $row;
488
+ }
489
+ return $return;
490
+ }
491
+
492
+ function escape($value) {
493
+ if (is_null($value)) {
494
+ return "NULL";
495
+ }
496
+ return "'" . $this->connection->real_escape_string($value) . "'";
497
+ }
498
+
499
+ function escape_like($search) {
500
+ return str_replace(array('_', '%'), array('\_', '\%'), $search);
501
+ }
502
+
503
+ function get_var($sql) {
504
+ $result = $this->query($sql);
505
+ $row = $result->fetch_array($result, MYSQLI_NUM);
506
+ return $row[0];
507
+ }
508
+
509
+ function fetch_row($data) {
510
+ return $data->fetch_array(MYSQLI_ASSOC);
511
+ }
512
+ }
513
+
514
+ class Shuttle_Exception extends Exception {};
readme.txt CHANGED
@@ -4,7 +4,7 @@ Contributors: WebFactory, wpreset, googlemapswidget, securityninja, underconstru
4
  Requires at least: 4.0
5
  Requires PHP: 5.2
6
  Tested up to: 4.9
7
- Stable tag: 1.35
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
@@ -12,16 +12,17 @@ WordPress Reset resets any WordPress site to the default values without modifyin
12
 
13
  == Description ==
14
 
15
- <a href="https://wpreset.com/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=wp-reset-top">WP Reset</a> quickly resets the site's database to the default installation values without modifying any files. It deletes all customizations and content. WP Reset is fast and safe to use and comes with a suite of useful developer tools. It has multiple fail-safe mechanisms so you can never accidentally lose data. WP Reset is extremely helpful for plugin & theme developers. It **speeds up development, testing and debugging** by providing a quick way to reset various settings and re-test code. It was developed by developers for developers.
16
 
17
  https://youtu.be/qMnkCW2PFoI?rel=0
18
 
19
- > WP Reset is proudly sponsored by <a target="_blank" href="https://ipgeolocation.io/">IP Geolocation</a>, a **premium GeoIP service for developers**. See how you can use their <a href="https://wpreset.com/geoip-transform-boring-data-better-user-experience/">GeoIP service</a> to make boring IP addresses more interesting for users. They offer <a href="https://ipgeolocation.io/signup">50,000 API requests a month FREE for developers</a>, and keep WP Reset updated & maintained.
20
-
21
  For support please use the <a href="https://wordpress.org/support/plugin/wp-reset">forums</a>, and if you need more information visit <a href="https://wpreset.com/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=wpreset.com">wpreset.com</a> and be sure to check out the <a href="https://wpreset.com/roadmap/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=roadmap">roadmap</a> for the list of upcoming features.
22
 
23
  Access WP Reset admin page via the "Tools" menu.
24
 
 
 
 
25
  **Please read carefully before proceeding to understand what WP Reset does**
26
 
27
  #### Resetting will delete:
@@ -49,6 +50,14 @@ Access WP Reset admin page via the "Tools" menu.
49
 
50
  WP Reset comes with full WP-CLI support. Help on our WP-CLI commands is available via _wp help reset_. By default the commands have to be confirmed but you can use the `--yes` option to skip confirmation. Instead of the active user, the first user with admin privileges found in the database will be restored after reset. Please be careful when using WP Reset with WP-CLI - as with using the GUI there is no undo.
51
 
 
 
 
 
 
 
 
 
52
  #### Multisite (WP-MU) Support
53
 
54
  WP Reset has yet to be completely tested with multisite! Please be careful when using it with multisite enabled. We don't recommend to resetting the main site. Sub-sites should be OK. We're working on making WP Reset fully compatible with WP-MU. Till then please be careful. Thank you for understanding.
@@ -89,6 +98,12 @@ Or if needed, upload manually;
89
  3. Additional tools for resetting and deleting various WordPress objects
90
 
91
  == Changelog ==
 
 
 
 
 
 
92
 
93
  = v1.35 =
94
  * 2018/09/18
4
  Requires at least: 4.0
5
  Requires PHP: 5.2
6
  Tested up to: 4.9
7
+ Stable tag: 1.40
8
  License: GPLv2 or later
9
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
10
 
12
 
13
  == Description ==
14
 
15
+ <a href="https://wpreset.com/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=wp-reset-top">WP Reset</a> quickly resets the site's database to the default installation values without modifying any files. It deletes all customizations and content. WP Reset is fast and safe to use. It has multiple fail-safe mechanisms so you can never accidentally lose data. WP Reset is extremely helpful for plugin and theme developers. It **speeds up testing and debugging** by providing a quick way to reset settings and re-test code. It was developed by developers for developers.
16
 
17
  https://youtu.be/qMnkCW2PFoI?rel=0
18
 
 
 
19
  For support please use the <a href="https://wordpress.org/support/plugin/wp-reset">forums</a>, and if you need more information visit <a href="https://wpreset.com/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=wpreset.com">wpreset.com</a> and be sure to check out the <a href="https://wpreset.com/roadmap/?utm_source=wordpressorg&utm_medium=content&utm_campaign=wp-reset&utm_term=roadmap">roadmap</a> for the list of upcoming features.
20
 
21
  Access WP Reset admin page via the "Tools" menu.
22
 
23
+ > WP Reset is proudly sponsored by <a target="_blank" href="https://ipgeolocation.io/">IP Geolocation</a>, a **premium GeoIP service for developers**. See how you can use their <a href="https://wpreset.com/geoip-transform-boring-data-better-user-experience/">GeoIP service</a> to make boring IP addresses more interesting for users. They offer <a href="https://ipgeolocation.io/signup">50,000 API requests a month FREE for developers</a>, and keep WP Reset updated & maintained.
24
+
25
+
26
  **Please read carefully before proceeding to understand what WP Reset does**
27
 
28
  #### Resetting will delete:
50
 
51
  WP Reset comes with full WP-CLI support. Help on our WP-CLI commands is available via _wp help reset_. By default the commands have to be confirmed but you can use the `--yes` option to skip confirmation. Instead of the active user, the first user with admin privileges found in the database will be restored after reset. Please be careful when using WP Reset with WP-CLI - as with using the GUI there is no undo.
52
 
53
+ #### Database Snapshots
54
+
55
+ Database snapshot is a copy of all WP database tables, standard and custom ones, saved in the currently used database (as set by _wp-config.php_). Files are not saved or included in snapshots in any way.
56
+ Snapshots are primarily a development tool. Although they can be used for backups (and downloaded as gzipped SQL dumps), we suggest finding a more suitable tool for doing backups of live sites. Use snapshots to find out what changes a plugin made to your database - what custom tables were created, modified, deleted or what changes were made to site's settings. Or use it to quickly restore the development environment after testing database related changes.
57
+ Restoring a snapshot does not affect other snapshots, or WP Reset settings. Snapshots can be compared to current database tables, restored (by overwriting current tables), exported ad gzipped SQL dumps, or deleted. Creating a snapshot on an average WordPress installation takes 1-2 seconds.
58
+
59
+ https://youtu.be/xBfMmS12vMY
60
+
61
  #### Multisite (WP-MU) Support
62
 
63
  WP Reset has yet to be completely tested with multisite! Please be careful when using it with multisite enabled. We don't recommend to resetting the main site. Sub-sites should be OK. We're working on making WP Reset fully compatible with WP-MU. Till then please be careful. Thank you for understanding.
98
  3. Additional tools for resetting and deleting various WordPress objects
99
 
100
  == Changelog ==
101
+ = v1.40 =
102
+ * 2018/10/24
103
+ * new tool: DB Snapshots
104
+ * rewrote code documentation for most functions
105
+ * some parts of Snapshots need refactoring
106
+ * 70k users hit on 2018/10/16 with 373,300 downloads; 30 days for +10k & 50k downloads
107
 
108
  = v1.35 =
109
  * 2018/09/18
wp-reset.php CHANGED
@@ -3,7 +3,7 @@
3
  Plugin Name: WP Reset
4
  Plugin URI: https://wpreset.com/
5
  Description: Reset the site to default installation values without modifying any files. Deletes all customizations and content.
6
- Version: 1.35
7
  Author: WebFactory Ltd
8
  Author URI: https://www.webfactoryltd.com/
9
  Text Domain: wp-reset
@@ -26,7 +26,7 @@
26
 
27
  // include only file
28
  if (!defined('ABSPATH')) {
29
- wp_die(__('Do not open this file directly.', 'wp-error'));
30
  }
31
 
32
 
@@ -41,8 +41,10 @@ class WP_Reset {
41
  public $version = 0;
42
  public $plugin_url = '';
43
  public $plugin_dir = '';
 
44
  protected $options = array();
45
  private $delete_count = 0;
 
46
 
47
 
48
  /**
@@ -79,6 +81,8 @@ class WP_Reset {
79
  add_filter('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'plugin_action_links'));
80
  add_filter('plugin_row_meta', array($this, 'plugin_meta_links'), 10, 2);
81
  add_filter('admin_footer_text', array($this, 'admin_footer_text'));
 
 
82
  } // __construct
83
 
84
 
@@ -142,6 +146,8 @@ class WP_Reset {
142
  /**
143
  * Get all dismissed notices, or check for one specific notice
144
  *
 
 
145
  * @return bool|array
146
  */
147
  function get_dismissed_notices($notice_name = '') {
@@ -164,6 +170,8 @@ class WP_Reset {
164
  *
165
  * todo: not completed
166
  *
 
 
167
  * @return array
168
  */
169
  function get_options($key = '') {
@@ -176,6 +184,9 @@ class WP_Reset {
176
  *
177
  * todo: this handles the entire options array although it should only do the options part - it's confusing
178
  *
 
 
 
179
  * @return bool
180
  */
181
  function update_options($key, $data) {
@@ -216,6 +227,8 @@ class WP_Reset {
216
  /**
217
  * Dismiss notice by adding it to dismissed_notices options array
218
  *
 
 
219
  * @return bool
220
  */
221
  function dismiss_notice($notice_name) {
@@ -278,6 +291,7 @@ class WP_Reset {
278
  $options = $this->get_options();
279
 
280
  $js_localize = array('undocumented_error' => __('An undocumented error has occured. Please refresh the page and try again.', 'wp-reset'),
 
281
  'plugin_name' => __('WP Reset', 'wp-reset'),
282
  'settings_url' => admin_url('tools.php?page=wp-reset'),
283
  'icon_url' => $this->plugin_url . 'img/wp-reset-icon.png',
@@ -335,12 +349,12 @@ class WP_Reset {
335
  /**
336
  * Deletes all transients.
337
  *
338
- * @return int
339
- */
340
- function do_delete_transients() {
341
  global $wpdb;
342
 
343
- $count = $wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE '\_transient\_%' OR option_name LIKE '\_site\_transient\_%'");
344
 
345
  return $count;
346
  } // do_delete_transients
@@ -349,22 +363,25 @@ class WP_Reset {
349
  /**
350
  * Deletes all files in uploads folder.
351
  *
352
- * @return int
353
- */
354
- function do_delete_uploads() {
355
  $upload_dir = wp_get_upload_dir();
356
 
357
  $this->delete_folder($upload_dir['basedir'], $upload_dir['basedir']);
358
 
359
  return $this->delete_count;
360
- } // do_delete_uploads
361
 
362
 
363
  /**
364
  * Recursively deletes a folder
365
  *
 
 
 
366
  * @return bool
367
- */
368
  private function delete_folder($folder, $base_folder) {
369
  $files = array_diff(scandir($folder), array('.', '..'));
370
 
@@ -375,7 +392,7 @@ class WP_Reset {
375
  $tmp = @unlink($folder . DIRECTORY_SEPARATOR . $file);
376
  $this->delete_count = $this->delete_count + (int) $tmp;
377
  }
378
- } // foreach
379
 
380
  if ($folder != $base_folder) {
381
  $tmp = @rmdir($folder);
@@ -390,9 +407,10 @@ class WP_Reset {
390
  /**
391
  * Deactivate and delete all plugins
392
  *
393
- * @param bool Keep WP Reset active and installed
394
- * @param bool Skip individual plugin deactivation functions when deactivating
395
- * @return int
 
396
  */
397
  function do_delete_plugins($keep_wp_reset = true, $silent_deactivate = false) {
398
  if (!function_exists('get_plugins')) {
@@ -425,8 +443,9 @@ class WP_Reset {
425
  /**
426
  * Delete all themes
427
  *
428
- * @param bool Keep default theme
429
- * @return int
 
430
  */
431
  function do_delete_themes($keep_default_theme = true) {
432
  $default_theme = 'twentyseventeen';
@@ -450,7 +469,7 @@ class WP_Reset {
450
 
451
 
452
  /**
453
- * Run tool via AJAX call
454
  *
455
  * @return null
456
  */
@@ -458,6 +477,7 @@ class WP_Reset {
458
  check_ajax_referer('wp-reset_run_tool');
459
 
460
  $tool = trim(@$_GET['tool']);
 
461
 
462
  if ($tool == 'delete_transients') {
463
  $cnt = $this->do_delete_transients();
@@ -471,6 +491,42 @@ class WP_Reset {
471
  } elseif ($tool == 'delete_uploads') {
472
  $cnt = $this->do_delete_uploads();
473
  wp_send_json_success($cnt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  } else {
475
  wp_send_json_error(__('Unknown tool.', 'wp-reset'));
476
  }
@@ -482,6 +538,8 @@ class WP_Reset {
482
  * There are no failsafes in the function - it reinstalls when called
483
  * Redirects when done
484
  *
 
 
485
  * @return null
486
  */
487
  function do_reinstall($params = array()) {
@@ -504,6 +562,7 @@ class WP_Reset {
504
  $wplang = get_option('wplang');
505
  $siteurl = get_option('siteurl');
506
  $home = get_option('home');
 
507
 
508
  $active_plugins = get_option('active_plugins');
509
  $active_theme = wp_get_theme();
@@ -537,6 +596,7 @@ class WP_Reset {
537
  update_option('siteurl', $siteurl);
538
  update_option('home', $home);
539
  update_option('wp-reset', $this->options);
 
540
 
541
  // remove password nag
542
  if (get_user_meta($user_id, 'default_password_nag')) {
@@ -583,7 +643,7 @@ class WP_Reset {
583
  * Checks wp_reset post value and performs all actions
584
  * todo: handle messages for various actions
585
  *
586
- * @return null
587
  */
588
  function do_all_actions() {
589
  // only admins can perform actions
@@ -616,12 +676,14 @@ class WP_Reset {
616
 
617
  $this->do_reinstall($params);
618
  }
619
- } // do_reset
620
 
621
 
622
  /**
623
  * Add "Reset WordPress" action link to plugins table, left part
624
  *
 
 
625
  * @return array
626
  */
627
  function plugin_action_links($links) {
@@ -636,6 +698,9 @@ class WP_Reset {
636
  /**
637
  * Add links to plugin's description in plugins table
638
  *
 
 
 
639
  * @return array
640
  */
641
  function plugin_meta_links($links, $file) {
@@ -645,16 +710,18 @@ class WP_Reset {
645
 
646
  $support_link = '<a target="_blank" href="https://wordpress.org/support/plugin/wp-reset" title="' . __('Get help', 'wp-reset') . '">' . __('Support', 'wp-reset') . '</a>';
647
  $home_link = '<a target="_blank" href="' . $this->generate_web_link('plugins-table-right') . '" title="' . __('Plugin Homepage', 'wp-reset') . '">' . __('Plugin Homepage', 'wp-reset') . '</a>';
 
648
 
649
  $links[] = $support_link;
650
  $links[] = $home_link;
 
651
 
652
  return $links;
653
  } // plugin_meta_links
654
 
655
 
656
  /**
657
- * Test if we're on plugin's admin page
658
  *
659
  * @return bool
660
  */
@@ -672,7 +739,9 @@ class WP_Reset {
672
  /**
673
  * Add powered by text in admin footer
674
  *
675
- * @return bool
 
 
676
  */
677
  function admin_footer_text($text) {
678
  if (!$this->is_plugin_page()) {
@@ -686,7 +755,7 @@ class WP_Reset {
686
 
687
 
688
  /**
689
- * Loads a plugin's translated strings
690
  *
691
  * @return null
692
  */
@@ -716,6 +785,7 @@ class WP_Reset {
716
  $notice_shown = false;
717
  $meta = $this->get_meta();
718
  $notices = $this->get_dismissed_notices();
 
719
 
720
  // double check for admin priv
721
  if (!current_user_can('administrator')) {
@@ -729,13 +799,13 @@ class WP_Reset {
729
 
730
  if (false === $notice_shown && is_multisite()) {
731
  echo '<div class="card notice-wrapper notice-error">';
732
- echo '<h2>' . __('WP Reset has not been fully tested with multisite', 'wp-reset') . '</h2>';
733
  echo '<p>' . __('Please be careful when using WP Reset with multisite enabled. It\'s not recommended to reset the main site. Sub-sites should be OK. We\'re working on making it fully compatible with WP-MU. <b>Till then please be careful.</b> Thank you for understanding.', 'wp-reset') . '</p>';
734
  echo '</div>';
735
  $notice_shown = true;
736
  }
737
 
738
- if (!empty($meta['reset_count']) && false === $notice_shown && false == $this->get_dismissed_notices('rate')) {
739
  echo '<div class="card notice-wrapper">';
740
  echo '<h2>' . __('Please help us keep the plugin free &amp; up-to-date', 'wp-reset') . '</h2>';
741
  echo '<p>' . __('If you use &amp; enjoy WP Reset, <b>please rate it on WordPress.org</b>. It only takes a second and helps us keep the plugin free and maintained. Thank you!', 'wp-reset') . '</p>';
@@ -744,6 +814,7 @@ class WP_Reset {
744
  $notice_shown = true;
745
  }
746
 
 
747
  // disabled for now
748
  if (false && false === $notice_shown && $meta['reset_count'] >= 2 && false == $this->get_dismissed_notices('tidy')) {
749
  echo '<div class="card notice-wrapper">';
@@ -760,6 +831,7 @@ class WP_Reset {
760
  echo '<ul class="wpr-main-tab">';
761
  echo '<li><a href="#tab-reset">' . __('Reset', 'wp-reset') . '</a></li>';
762
  echo '<li><a href="#tab-tools">' . __('Tools', 'wp-reset') . '</a></li>';
 
763
  echo '<li><a href="#tab-support">' . __('Support', 'wp-reset') . '</a></li>';
764
  if (empty($notices['geoip_tab'])) {
765
  echo '<li><a href="#tab-geoip">' . __('IP Geolocation', 'wp-reset') . '</a></li>';
@@ -773,6 +845,10 @@ class WP_Reset {
773
  echo '<div style="display: none;" id="tab-tools">';
774
  $this->tab_tools();
775
  echo '</div>';
 
 
 
 
776
 
777
  echo '<div style="display: none;" id="tab-support">';
778
  $this->tab_support();
@@ -908,9 +984,101 @@ class WP_Reset {
908
  echo '<h2>' . __('Private contact', 'wp-reset') . '</h2>';
909
  echo '<p>' . __('If there\'s a need to contact us privately send emails to <a href="mailto:wpreset@webfactoryltd.com">wpreset@webfactoryltd.com</a>. Please know that although we\'ll gladly have a look at issues you are having with any site, we can\'t promise we\'ll fix them. Thank you for understanding.', 'wp-reset') . '</p>';
910
  echo '</div>';
 
 
 
 
 
911
  } // tab_support
912
 
913
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
914
  /**
915
  * Echoes content for sponsor tab
916
  *
@@ -941,6 +1109,11 @@ class WP_Reset {
941
  /**
942
  * Helper function for generating UTM tagged links
943
  *
 
 
 
 
 
944
  * @return string
945
  */
946
  function generate_web_link($placement = '', $page = '/', $params = array(), $anchor = '') {
@@ -965,6 +1138,520 @@ class WP_Reset {
965
  } // generate_web_link
966
 
967
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
968
  /**
969
  * Clean up on uninstall; no action on deactive at the moment
970
  *
@@ -972,6 +1659,7 @@ class WP_Reset {
972
  */
973
  static function uninstall() {
974
  delete_option('wp-reset');
 
975
  } // uninstall
976
 
977
 
3
  Plugin Name: WP Reset
4
  Plugin URI: https://wpreset.com/
5
  Description: Reset the site to default installation values without modifying any files. Deletes all customizations and content.
6
+ Version: 1.40
7
  Author: WebFactory Ltd
8
  Author URI: https://www.webfactoryltd.com/
9
  Text Domain: wp-reset
26
 
27
  // include only file
28
  if (!defined('ABSPATH')) {
29
+ wp_die(__('Do not open this file directly.', 'wp-error'));
30
  }
31
 
32
 
41
  public $version = 0;
42
  public $plugin_url = '';
43
  public $plugin_dir = '';
44
+ public $snapshots_folder = 'wp-reset-snapshots-export';
45
  protected $options = array();
46
  private $delete_count = 0;
47
+ private $core_tables = array('commentmeta', 'comments', 'links', 'options', 'postmeta', 'posts', 'term_relationships', 'term_taxonomy', 'termmeta', 'terms', 'usermeta', 'users');
48
 
49
 
50
  /**
81
  add_filter('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'plugin_action_links'));
82
  add_filter('plugin_row_meta', array($this, 'plugin_meta_links'), 10, 2);
83
  add_filter('admin_footer_text', array($this, 'admin_footer_text'));
84
+
85
+ $this->core_tables = array_map(function($tbl) { global $wpdb; return $wpdb->prefix . $tbl; }, $this->core_tables);
86
  } // __construct
87
 
88
 
146
  /**
147
  * Get all dismissed notices, or check for one specific notice
148
  *
149
+ * @param string $notice_name Optional. Check if specified notice is dismissed.
150
+ *
151
  * @return bool|array
152
  */
153
  function get_dismissed_notices($notice_name = '') {
170
  *
171
  * todo: not completed
172
  *
173
+ * @param string $key Optional.
174
+ *
175
  * @return array
176
  */
177
  function get_options($key = '') {
184
  *
185
  * todo: this handles the entire options array although it should only do the options part - it's confusing
186
  *
187
+ * @param string $key Data to save.
188
+ * @param string $data Option key.
189
+ *
190
  * @return bool
191
  */
192
  function update_options($key, $data) {
227
  /**
228
  * Dismiss notice by adding it to dismissed_notices options array
229
  *
230
+ * @param string $notice_name Notice to dismiss.
231
+ *
232
  * @return bool
233
  */
234
  function dismiss_notice($notice_name) {
291
  $options = $this->get_options();
292
 
293
  $js_localize = array('undocumented_error' => __('An undocumented error has occured. Please refresh the page and try again.', 'wp-reset'),
294
+ 'documented_error' => __('An error has occured.', 'wp-reset'),
295
  'plugin_name' => __('WP Reset', 'wp-reset'),
296
  'settings_url' => admin_url('tools.php?page=wp-reset'),
297
  'icon_url' => $this->plugin_url . 'img/wp-reset-icon.png',
349
  /**
350
  * Deletes all transients.
351
  *
352
+ * @return int Number of deleted transient DB entries
353
+ */
354
+ function do_delete_transients() {
355
  global $wpdb;
356
 
357
+ $count = $wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE '\_transient\_%' OR option_name LIKE '\_site\_transient\_%'");
358
 
359
  return $count;
360
  } // do_delete_transients
363
  /**
364
  * Deletes all files in uploads folder.
365
  *
366
+ * @return int Number of deleted files and folders.
367
+ */
368
+ function do_delete_uploads() {
369
  $upload_dir = wp_get_upload_dir();
370
 
371
  $this->delete_folder($upload_dir['basedir'], $upload_dir['basedir']);
372
 
373
  return $this->delete_count;
374
+ } // do_delete_uploads
375
 
376
 
377
  /**
378
  * Recursively deletes a folder
379
  *
380
+ * @param string $folder Recursive param.
381
+ * @param string $base_folder Base folder.
382
+ *
383
  * @return bool
384
+ */
385
  private function delete_folder($folder, $base_folder) {
386
  $files = array_diff(scandir($folder), array('.', '..'));
387
 
392
  $tmp = @unlink($folder . DIRECTORY_SEPARATOR . $file);
393
  $this->delete_count = $this->delete_count + (int) $tmp;
394
  }
395
+ } // foreach
396
 
397
  if ($folder != $base_folder) {
398
  $tmp = @rmdir($folder);
407
  /**
408
  * Deactivate and delete all plugins
409
  *
410
+ * @param bool $keep_wp_reset Keep WP Reset active and installed
411
+ * @param bool $silent_deactivate Skip individual plugin deactivation functions when deactivating
412
+ *
413
+ * @return int Number of deleted plugins.
414
  */
415
  function do_delete_plugins($keep_wp_reset = true, $silent_deactivate = false) {
416
  if (!function_exists('get_plugins')) {
443
  /**
444
  * Delete all themes
445
  *
446
+ * @param bool $keep_default_theme Keep default theme
447
+ *
448
+ * @return int Number of deleted themes.
449
  */
450
  function do_delete_themes($keep_default_theme = true) {
451
  $default_theme = 'twentyseventeen';
469
 
470
 
471
  /**
472
+ * Run one tool via AJAX call
473
  *
474
  * @return null
475
  */
477
  check_ajax_referer('wp-reset_run_tool');
478
 
479
  $tool = trim(@$_GET['tool']);
480
+ $extra_data = trim(@$_GET['extra_data']);
481
 
482
  if ($tool == 'delete_transients') {
483
  $cnt = $this->do_delete_transients();
491
  } elseif ($tool == 'delete_uploads') {
492
  $cnt = $this->do_delete_uploads();
493
  wp_send_json_success($cnt);
494
+ } elseif ($tool == 'delete_snapshot') {
495
+ $res = $this->do_delete_snapshot($extra_data);
496
+ if (is_wp_error($res)) {
497
+ wp_send_json_error($res->get_error_message());
498
+ } else {
499
+ wp_send_json_success();
500
+ }
501
+ } elseif ($tool == 'download_snapshot') {
502
+ $res = $this->do_export_snapshot($extra_data);
503
+ if (is_wp_error($res)) {
504
+ wp_send_json_error($res->get_error_message());
505
+ } else {
506
+ $url = content_url() . '/' . $this->snapshots_folder . '/' . $res;
507
+ wp_send_json_success($url);
508
+ }
509
+ } elseif ($tool == 'restore_snapshot') {
510
+ $res = $this->do_restore_snapshot($extra_data);
511
+ if (is_wp_error($res)) {
512
+ wp_send_json_error($res->get_error_message());
513
+ } else {
514
+ wp_send_json_success();
515
+ }
516
+ } elseif ($tool == 'compare_snapshots') {
517
+ $res = $this->do_compare_snapshots($extra_data);
518
+ if (is_wp_error($res)) {
519
+ wp_send_json_error($res->get_error_message());
520
+ } else {
521
+ wp_send_json_success($res);
522
+ }
523
+ } elseif ($tool == 'create_snapshot') {
524
+ $res = $this->do_create_snapshot($extra_data);
525
+ if (is_wp_error($res)) {
526
+ wp_send_json_error($res->get_error_message());
527
+ } else {
528
+ wp_send_json_success();
529
+ }
530
  } else {
531
  wp_send_json_error(__('Unknown tool.', 'wp-reset'));
532
  }
538
  * There are no failsafes in the function - it reinstalls when called
539
  * Redirects when done
540
  *
541
+ * @param array $params Optional.
542
+ *
543
  * @return null
544
  */
545
  function do_reinstall($params = array()) {
562
  $wplang = get_option('wplang');
563
  $siteurl = get_option('siteurl');
564
  $home = get_option('home');
565
+ $snapshots = $this->get_snapshots();
566
 
567
  $active_plugins = get_option('active_plugins');
568
  $active_theme = wp_get_theme();
596
  update_option('siteurl', $siteurl);
597
  update_option('home', $home);
598
  update_option('wp-reset', $this->options);
599
+ update_option('wp-reset-snapshots', $snapshots);
600
 
601
  // remove password nag
602
  if (get_user_meta($user_id, 'default_password_nag')) {
643
  * Checks wp_reset post value and performs all actions
644
  * todo: handle messages for various actions
645
  *
646
+ * @return null|bool
647
  */
648
  function do_all_actions() {
649
  // only admins can perform actions
676
 
677
  $this->do_reinstall($params);
678
  }
679
+ } // do_all_actions
680
 
681
 
682
  /**
683
  * Add "Reset WordPress" action link to plugins table, left part
684
  *
685
+ * @param array $links Initial list of links.
686
+ *
687
  * @return array
688
  */
689
  function plugin_action_links($links) {
698
  /**
699
  * Add links to plugin's description in plugins table
700
  *
701
+ * @param array $links Initial list of links.
702
+ * @param string $file Basename of current plugin.
703
+ *
704
  * @return array
705
  */
706
  function plugin_meta_links($links, $file) {
710
 
711
  $support_link = '<a target="_blank" href="https://wordpress.org/support/plugin/wp-reset" title="' . __('Get help', 'wp-reset') . '">' . __('Support', 'wp-reset') . '</a>';
712
  $home_link = '<a target="_blank" href="' . $this->generate_web_link('plugins-table-right') . '" title="' . __('Plugin Homepage', 'wp-reset') . '">' . __('Plugin Homepage', 'wp-reset') . '</a>';
713
+ $rate_link = '<a target="_blank" href="https://wordpress.org/support/plugin/wp-reset/reviews/#new-post" title="' . __('Rate the plugin', 'wp-reset') . '">' . __('Rate the plugin ★★★★★', 'wp-reset') . '</a>';
714
 
715
  $links[] = $support_link;
716
  $links[] = $home_link;
717
+ $links[] = $rate_link;
718
 
719
  return $links;
720
  } // plugin_meta_links
721
 
722
 
723
  /**
724
+ * Test if we're on WPR's admin page
725
  *
726
  * @return bool
727
  */
739
  /**
740
  * Add powered by text in admin footer
741
  *
742
+ * @param string $text Default footer text.
743
+ *
744
+ * @return string
745
  */
746
  function admin_footer_text($text) {
747
  if (!$this->is_plugin_page()) {
755
 
756
 
757
  /**
758
+ * Loads plugin's translated strings
759
  *
760
  * @return null
761
  */
785
  $notice_shown = false;
786
  $meta = $this->get_meta();
787
  $notices = $this->get_dismissed_notices();
788
+ $snapshots = $this->get_snapshots();
789
 
790
  // double check for admin priv
791
  if (!current_user_can('administrator')) {
799
 
800
  if (false === $notice_shown && is_multisite()) {
801
  echo '<div class="card notice-wrapper notice-error">';
802
+ echo '<h2>' . __('WP Reset is not compatible with multisite!', 'wp-reset') . '</h2>';
803
  echo '<p>' . __('Please be careful when using WP Reset with multisite enabled. It\'s not recommended to reset the main site. Sub-sites should be OK. We\'re working on making it fully compatible with WP-MU. <b>Till then please be careful.</b> Thank you for understanding.', 'wp-reset') . '</p>';
804
  echo '</div>';
805
  $notice_shown = true;
806
  }
807
 
808
+ if ((!empty($meta['reset_count']) || !empty($snapshots)) && false === $notice_shown && false == $this->get_dismissed_notices('rate')) {
809
  echo '<div class="card notice-wrapper">';
810
  echo '<h2>' . __('Please help us keep the plugin free &amp; up-to-date', 'wp-reset') . '</h2>';
811
  echo '<p>' . __('If you use &amp; enjoy WP Reset, <b>please rate it on WordPress.org</b>. It only takes a second and helps us keep the plugin free and maintained. Thank you!', 'wp-reset') . '</p>';
814
  $notice_shown = true;
815
  }
816
 
817
+ // Tidy Repo ad
818
  // disabled for now
819
  if (false && false === $notice_shown && $meta['reset_count'] >= 2 && false == $this->get_dismissed_notices('tidy')) {
820
  echo '<div class="card notice-wrapper">';
831
  echo '<ul class="wpr-main-tab">';
832
  echo '<li><a href="#tab-reset">' . __('Reset', 'wp-reset') . '</a></li>';
833
  echo '<li><a href="#tab-tools">' . __('Tools', 'wp-reset') . '</a></li>';
834
+ echo '<li><a href="#tab-snapshots">' . __('DB Snapshots', 'wp-reset') . '</a></li>';
835
  echo '<li><a href="#tab-support">' . __('Support', 'wp-reset') . '</a></li>';
836
  if (empty($notices['geoip_tab'])) {
837
  echo '<li><a href="#tab-geoip">' . __('IP Geolocation', 'wp-reset') . '</a></li>';
845
  echo '<div style="display: none;" id="tab-tools">';
846
  $this->tab_tools();
847
  echo '</div>';
848
+
849
+ echo '<div style="display: none;" id="tab-snapshots">';
850
+ $this->tab_snapshots();
851
+ echo '</div>';
852
 
853
  echo '<div style="display: none;" id="tab-support">';
854
  $this->tab_support();
984
  echo '<h2>' . __('Private contact', 'wp-reset') . '</h2>';
985
  echo '<p>' . __('If there\'s a need to contact us privately send emails to <a href="mailto:wpreset@webfactoryltd.com">wpreset@webfactoryltd.com</a>. Please know that although we\'ll gladly have a look at issues you are having with any site, we can\'t promise we\'ll fix them. Thank you for understanding.', 'wp-reset') . '</p>';
986
  echo '</div>';
987
+
988
+ echo '<div class="card">';
989
+ echo '<h2>' . __('Care to help out?', 'wp-reset') . '</h2>';
990
+ echo '<p>' . __('No need for donations or anything like that :) If you can give us a <a href="https://wordpress.org/support/plugin/wp-reset/reviews/#new-post" target="_blank">five star rating</a> you\'ll help out more than you can imagine. Thank you!', 'wp-reset') . '</p>';
991
+ echo '</div>';
992
  } // tab_support
993
 
994
 
995
+ /**
996
+ * Echoes content for snapshots tab
997
+ *
998
+ * @return null
999
+ */
1000
+ private function tab_snapshots() {
1001
+ global $wpdb;
1002
+ $tbl_core = $tbl_custom = $tbl_size = $tbl_rows = 0;
1003
+
1004
+ echo '<div class="card" id="card-snapshots">';
1005
+ echo '<a class="toggle-card" href="#" title="' . __('Collapse / expand box', 'wp-reset') . '"><span class="dashicons dashicons-arrow-up-alt2"></span></a>';
1006
+ echo '<h2>' . __('Database Snapshots', 'wp-reset') . '</h2>';
1007
+ echo '<p>A snapshot is a copy of all WP database tables, standard and custom ones, saved in your database. Files are not saved or included in snapshots in any way.<br>
1008
+ Snapshots are primarily a development tool. Although they can be used for backups (and downloaded), we suggest finding a more suitable tool for live sites, such as <a href="https://wordpress.org/plugins/updraftplus/" target="_blank">UpdraftPlus</a>. Use snapshots to find out what changes a plugin made to your database or to quickly restore the dev environment after testing database related changes.<br>Restoring a snapshot does not affect other snapshots, or WP Reset settings.</p>';
1009
+ echo '<p>Snapshots are still in development. If you see a bug or just have an idea how to make the tool better, please let us know <a href="https://twitter.com/WebFactoryLtd" target="_blank">@webfactoryltd</a> or <a href="mailto:wpreset@webfactoryltd.com?subject=WPR%20DB%20Snapshots%20Feedback">email us</a>. Thank you!</p>';
1010
+
1011
+ $table_status = $wpdb->get_results('SHOW TABLE STATUS');
1012
+ if (is_array($table_status)) {
1013
+ foreach ($table_status as $index => $table) {
1014
+ if (0 !== stripos($table->Name, $wpdb->prefix)) {
1015
+ continue;
1016
+ }
1017
+ if (empty($table->Engine)) {
1018
+ continue;
1019
+ }
1020
+
1021
+ $tbl_rows += $table->Rows;
1022
+ $tbl_size += $table->Data_length + $table->Index_length;
1023
+ if (in_array($table->Name, $this->core_tables)) {
1024
+ $tbl_core++;
1025
+ } else {
1026
+ $tbl_custom++;
1027
+ }
1028
+ } // foreach
1029
+
1030
+ echo '<p><b>Currently used WordPress tables</b>, prefixed with <i>' . $wpdb->prefix . '</i>, consist of ' . $tbl_core . ' standard and ';
1031
+ if ($tbl_custom) {
1032
+ echo $tbl_custom . ' custom table' . ($tbl_custom == 1? '': 's');
1033
+ } else {
1034
+ echo 'no custom tables';
1035
+ }
1036
+ echo ' totaling ' . $this->format_size($tbl_size) .' in ' . number_format($tbl_rows) . ' rows.</p>';
1037
+ }
1038
+
1039
+ echo '';
1040
+ echo '</div>';
1041
+
1042
+ echo '<div class="card no-padding-bottom">';
1043
+ echo '<a id="create-new-snapshot-primary" data-msg-success="Snapshot created!" data-msg-wait="Creating snapshot. Please wait." data-btn-confirm="Create snapshot" data-placeholder="Snapshot name or brief description, ie: before plugin install" data-text="Enter snapshot name or brief description, up to 64 characters." data-title="Create a new snapshot" title="Create a new database snapshot" href="#" class="button button-primary create-new-snapshot create-new-snapshot-corner">' . __('Create new', 'wp-reset') . '</a>';
1044
+ echo '<h2>' . __('Saved Snapshots', 'wp-reset') . '</h2>';
1045
+
1046
+ if ($snapshots = $this->get_snapshots()) {
1047
+ echo '<table id="wpr-snapshots">';
1048
+ echo '<tr><th>Name</th><th>Info &amp; Size</th><th class="ss-actions">Actions</th></tr>';
1049
+ foreach ($snapshots as $ss) {
1050
+ echo '<tr id="wpr-ss-' . $ss['uid'] . '">';
1051
+ if (!empty($ss['name'])) {
1052
+ echo '<td title="Created on ' . date(get_option('date_format'), strtotime($ss['timestamp'])) . ' @ ' . date(get_option('time_format'), strtotime($ss['timestamp'])) . '">' . $ss['name'] . '</td>';
1053
+ $name = $ss['name'];
1054
+ } else {
1055
+ echo '<td title="Created on ' . date(get_option('date_format'), strtotime($ss['timestamp'])) . ' @ ' . date(get_option('time_format'), strtotime($ss['timestamp'])) . '">' . '' . date(get_option('date_format'), strtotime($ss['timestamp'])) . '<br>@ ' . date(get_option('time_format'), strtotime($ss['timestamp'])) . '</td>';
1056
+ $name = 'created on ' . date(get_option('date_format'), strtotime($ss['timestamp'])) . ' @ ' . date(get_option('time_format'), strtotime($ss['timestamp']));
1057
+ }
1058
+ echo '<td>' . $ss['tbl_core'] . ' standard &amp; ';
1059
+ if ($ss['tbl_custom']) {
1060
+ echo $ss['tbl_custom'] . ' custom table' . ($ss['tbl_custom'] == 1? '': 's');
1061
+ } else {
1062
+ echo 'no custom tables';
1063
+ }
1064
+ echo ' totaling ' . $this->format_size($ss['tbl_size']) . ' in ' . number_format($ss['tbl_rows']) . ' rows</td>';
1065
+ echo '<td>';
1066
+ echo '<a data-title="Current DB tables compared to snapshot %s" data-wait-msg="Comparing. Please wait." data-name="' . $name . '" title="Compare snapshot to current database tables" href="#" class="ss-action compare-snapshot" data-ss-uid="' . $ss['uid'] . '"><span class="dashicons dashicons-visibility"></span></a>';
1067
+ echo '<a data-btn-confirm="Restore snapshot" data-text-wait="Restoring snapshot. Please wait." data-text-confirm="Are you sure you want to restore the selected snapshot? There is NO UNDO.<br>Restoring the snapshot will delete all current standard and custom tables and replace them with tables from the snapshot." data-text-done="Snapshot has been restored. Click OK to reload the page with new data." title="Restore snapshot by overwriting current database tables" href="#" class="ss-action restore-snapshot" data-ss-uid="' . $ss['uid'] . '"><span class="dashicons dashicons-backup"></span></a>';
1068
+ echo '<a data-success-msg="Snapshot export created!<br><a href=\'%s\'>Download it</a>" data-wait-msg="Exporting snapshot. Please wait." title="Download snapshot as gzipped SQL dump" href="#" class="ss-action download-snapshot" data-ss-uid="' . $ss['uid'] . '"><span class="dashicons dashicons-download"></span></a>';
1069
+ echo '<a data-btn-confirm="Delete snapshot" data-text-wait="Deleting snapshot. Please wait." data-text-confirm="Are you sure you want to delete the selected snapshot and all its data? There is NO UNDO.<br>Deleting the snapshot will not affect the active database tables in any way." data-text-done="Snapshot has been deleted." title="Permanently delete snapshot" href="#" class="ss-action delete-snapshot" data-ss-uid="' . $ss['uid'] . '"><span class="dashicons dashicons-trash"></span></a></td>';
1070
+ echo '</tr>';
1071
+ } // foreach
1072
+ echo '</table>';
1073
+ echo '<p id="ss-no-snapshots" class="hidden">There are no saved snapshots. <a href="#" class="create-new-snapshot">Create a new snapshot.</a></p>';
1074
+ } else {
1075
+ echo '<p id="ss-no-snapshots">There are no saved snapshots. <a href="#" class="create-new-snapshot">Create a new snapshot.</a></p>';
1076
+ }
1077
+
1078
+ echo '</div>';
1079
+ } // tab_snapshots
1080
+
1081
+
1082
  /**
1083
  * Echoes content for sponsor tab
1084
  *
1109
  /**
1110
  * Helper function for generating UTM tagged links
1111
  *
1112
+ * @param string $placement Optional. UTM content param.
1113
+ * @param string $page Optional. Page to link to.
1114
+ * @param array $params Optional. Extra URL params.
1115
+ * @param string $anchor Optional. URL anchor part.
1116
+ *
1117
  * @return string
1118
  */
1119
  function generate_web_link($placement = '', $page = '/', $params = array(), $anchor = '') {
1138
  } // generate_web_link
1139
 
1140
 
1141
+ /**
1142
+ * Returns all saved snapshots from DB
1143
+ *
1144
+ * @return array
1145
+ */
1146
+ function get_snapshots() {
1147
+ $snapshots = get_option('wp-reset-snapshots', array());
1148
+
1149
+ return $snapshots;
1150
+ } // get_snapshots
1151
+
1152
+
1153
+ /**
1154
+ * Format file size to human readable string
1155
+ *
1156
+ * @param int $bytes Size in bytes to format.
1157
+ *
1158
+ * @return string
1159
+ */
1160
+ function format_size($bytes) {
1161
+ if ($bytes > 1073741824) {
1162
+ return number_format_i18n($bytes / 1073741824, 2) . ' GB';
1163
+ } elseif ($bytes > 1048576) {
1164
+ return number_format_i18n($bytes / 1048576, 1) . ' MB';
1165
+ } elseif ($bytes > 1024) {
1166
+ return number_format_i18n($bytes / 1024, 1) . ' KB';
1167
+ } else {
1168
+ return number_format_i18n($bytes, 0) . ' bytes';
1169
+ }
1170
+ } // format_size
1171
+
1172
+
1173
+ /**
1174
+ * Creates snapshot of current tables by copying them in the DB and saving metadata.
1175
+ *
1176
+ * @param int $name Optional. Name for the new snapshot.
1177
+ *
1178
+ * @return array|WP_Error Snapshot details in array on success, or error object on fail.
1179
+ */
1180
+ function do_create_snapshot($name = '') {
1181
+ global $wpdb;
1182
+ $snapshots = $this->get_snapshots();
1183
+ $snapshot = array();
1184
+ $uid = $this->generate_snapshot_uid();
1185
+ $tbl_core = $tbl_custom = $tbl_size = $tbl_rows = 0;
1186
+
1187
+ if (!$uid) {
1188
+ return new WP_Error(1, 'Unable to generate a valid snapshot UID.');
1189
+ }
1190
+
1191
+ if ($name) {
1192
+ $snapshot['name'] = substr(trim($name), 0, 64);
1193
+ } else {
1194
+ $snapshot['name'] = '';
1195
+ }
1196
+ $snapshot['uid'] = $uid;
1197
+ $snapshot['timestamp'] = current_time('mysql');
1198
+
1199
+ $table_status = $wpdb->get_results('SHOW TABLE STATUS');
1200
+ if (is_array($table_status)) {
1201
+ foreach ($table_status as $index => $table) {
1202
+ if (0 !== stripos($table->Name, $wpdb->prefix)) {
1203
+ continue;
1204
+ }
1205
+ if (empty($table->Engine)) {
1206
+ continue;
1207
+ }
1208
+
1209
+ $tbl_rows += $table->Rows;
1210
+ $tbl_size += $table->Data_length + $table->Index_length;
1211
+ if (in_array($table->Name, $this->core_tables)) {
1212
+ $tbl_core++;
1213
+ } else {
1214
+ $tbl_custom++;
1215
+ }
1216
+
1217
+ $wpdb->query('OPTIMIZE TABLE ' . $table->Name);
1218
+ $wpdb->query('CREATE TABLE ' . $uid . '_' . $table->Name .' LIKE ' . $table->Name);
1219
+ $wpdb->query('INSERT ' . $uid . '_' . $table->Name . ' SELECT * FROM ' . $table->Name);
1220
+ } // foreach
1221
+ } else {
1222
+ return new WP_Error(1, 'Can\'t get table status data.');
1223
+ }
1224
+
1225
+ $snapshot['tbl_core'] = $tbl_core;
1226
+ $snapshot['tbl_custom'] = $tbl_custom;
1227
+ $snapshot['tbl_rows'] = $tbl_rows;
1228
+ $snapshot['tbl_size'] = $tbl_size;
1229
+
1230
+
1231
+ $snapshots[$uid] = $snapshot;
1232
+ update_option('wp-reset-snapshots', $snapshots);
1233
+
1234
+ return $snapshot;
1235
+ } // create_snapshot
1236
+
1237
+
1238
+ /**
1239
+ * Delete snapshot metadata and tables from DB
1240
+ *
1241
+ * @param string $uid Snapshot unique 6-char ID.
1242
+ *
1243
+ * @return bool|WP_Error True on success, or error object on fail.
1244
+ */
1245
+ function do_delete_snapshot($uid = '') {
1246
+ global $wpdb;
1247
+ $snapshots = $this->get_snapshots();
1248
+
1249
+ if (strlen($uid) != 6) {
1250
+ return new WP_Error(1, 'Invalid UID format.');
1251
+ }
1252
+
1253
+ if (!isset($snapshots[$uid])) {
1254
+ return new WP_Error(1, 'Unknown snapshot ID.');
1255
+ }
1256
+
1257
+ $tables = $wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', array($uid . '\_%')));
1258
+ foreach ($tables as $table) {
1259
+ $wpdb->query('DROP TABLE IF EXISTS ' . $table);
1260
+ }
1261
+
1262
+ unset($snapshots[$uid]);
1263
+ update_option('wp-reset-snapshots', $snapshots);
1264
+
1265
+ return true;
1266
+ } // delete_snapshot
1267
+
1268
+
1269
+ /**
1270
+ * Exports snapshot as SQL dump; saved in gzipped file in WP_CONTENT folder.
1271
+ *
1272
+ * @param string $uid Snapshot unique 6-char ID.
1273
+ *
1274
+ * @return string|WP_Error Export base filename, or error object on fail.
1275
+ */
1276
+ function do_export_snapshot($uid = '') {
1277
+ global $wpdb;
1278
+ $snapshots = $this->get_snapshots();
1279
+
1280
+ if (strlen($uid) != 6) {
1281
+ return new WP_Error(1, 'Invalid snapshot ID format.');
1282
+ }
1283
+
1284
+ if (!isset($snapshots[$uid])) {
1285
+ return new WP_Error(1, 'Unknown snapshot ID.');
1286
+ }
1287
+
1288
+ require_once $this->plugin_dir . 'libs/dumper.php';
1289
+
1290
+ try {
1291
+ $world_dumper = Shuttle_Dumper::create(array(
1292
+ 'host' => DB_HOST,
1293
+ 'username' => DB_USER,
1294
+ 'password' => DB_PASSWORD,
1295
+ 'db_name' => DB_NAME,
1296
+ ));
1297
+
1298
+ $folder = wp_mkdir_p(trailingslashit(WP_CONTENT_DIR) . $this->snapshots_folder);
1299
+ if (!$folder) {
1300
+ return new WP_Error(1, 'Unable to create wp-content/' . $this->snapshots_folder . '/ folder.');
1301
+ }
1302
+
1303
+ $world_dumper->dump(trailingslashit(WP_CONTENT_DIR) . $this->snapshots_folder . '/wp-reset-snapshot-' . $uid . '.sql.gz', $uid . '_');
1304
+ } catch(Shuttle_Exception $e) {
1305
+ return new WP_Error(1, "Couldn't dump snapshot: " . $e->getMessage());
1306
+ }
1307
+
1308
+ return 'wp-reset-snapshot-' . $uid . '.sql.gz';
1309
+ } // export_snapshot
1310
+
1311
+
1312
+ /**
1313
+ * Replace current tables with ones in snapshot.
1314
+ *
1315
+ * @param string $uid Snapshot unique 6-char ID.
1316
+ *
1317
+ * @return bool|WP_Error True on success, or error object on fail.
1318
+ */
1319
+ function do_restore_snapshot($uid = '') {
1320
+ global $wpdb;
1321
+ $new_tables = array();
1322
+ $snapshots = $this->get_snapshots();
1323
+
1324
+ if (($res = $this->verify_snapshot_integrity($uid)) !== true) {
1325
+ return $res;
1326
+ }
1327
+
1328
+ $table_status = $wpdb->get_results('SHOW TABLE STATUS');
1329
+ if (is_array($table_status)) {
1330
+ foreach ($table_status as $index => $table) {
1331
+ if (0 !== stripos($table->Name, $uid . '_')) {
1332
+ continue;
1333
+ }
1334
+ if (empty($table->Engine)) {
1335
+ continue;
1336
+ }
1337
+
1338
+ $new_tables[] = $table->Name;
1339
+ } // foreach
1340
+ } else {
1341
+ return new WP_Error(1, 'Can\'t get table status data.');
1342
+ }
1343
+
1344
+ foreach ($table_status as $index => $table) {
1345
+ if (0 !== stripos($table->Name, $wpdb->prefix)) {
1346
+ continue;
1347
+ }
1348
+ if (empty($table->Engine)) {
1349
+ continue;
1350
+ }
1351
+
1352
+ $wpdb->query('DROP TABLE ' . $table->Name);
1353
+ } // foreach
1354
+
1355
+ // copy snapshot tables to original name
1356
+ foreach ($new_tables as $table) {
1357
+ $new_name = str_replace($uid . '_', '', $table);
1358
+
1359
+ $wpdb->query('CREATE TABLE ' . $new_name . ' LIKE ' . $table);
1360
+ $wpdb->query('INSERT ' . $new_name . ' SELECT * FROM ' . $table);
1361
+ }
1362
+
1363
+ wp_cache_flush();
1364
+ update_option('wp-reset', $this->options);
1365
+ update_option('wp-reset-snapshots', $snapshots);
1366
+
1367
+ return true;
1368
+ } // restore_snapshot
1369
+
1370
+
1371
+ /**
1372
+ * Verifies snapshot integrity by comparing metadata and data in DB
1373
+ *
1374
+ * @param string $uid Snapshot unique 6-char ID.
1375
+ *
1376
+ * @return bool|WP_Error True on success, or error object on fail.
1377
+ */
1378
+ function verify_snapshot_integrity($uid) {
1379
+ global $wpdb;
1380
+ $tbl_core = $tbl_custom = 0;
1381
+ $snapshots = $this->get_snapshots();
1382
+
1383
+ if (strlen($uid) != 6) {
1384
+ return new WP_Error(1, 'Invalid snapshot ID format.');
1385
+ }
1386
+
1387
+ if (!isset($snapshots[$uid])) {
1388
+ return new WP_Error(1, 'Unknown snapshot ID.');
1389
+ }
1390
+
1391
+ $snapshot = $snapshots[$uid];
1392
+
1393
+ $table_status = $wpdb->get_results('SHOW TABLE STATUS');
1394
+ if (is_array($table_status)) {
1395
+ foreach ($table_status as $index => $table) {
1396
+ if (0 !== stripos($table->Name, $uid . '_')) {
1397
+ continue;
1398
+ }
1399
+ if (empty($table->Engine)) {
1400
+ continue;
1401
+ }
1402
+
1403
+ if (in_array(str_replace($uid . '_', '', $table->Name), $this->core_tables)) {
1404
+ $tbl_core++;
1405
+ } else {
1406
+ $tbl_custom++;
1407
+ }
1408
+ } // foreach
1409
+
1410
+ if ($tbl_core != $snapshot['tbl_core'] || $tbl_custom != $snapshot['tbl_custom']) {
1411
+ return new WP_Error(1, 'Snapshot data has been compromised. Saved metadata does not match data in the DB. Contact WP Reset support if data is critical, or restore it via a MySQL GUI.');
1412
+ }
1413
+ } else {
1414
+ return new WP_Error(1, 'Can\'t get table status data.');
1415
+ }
1416
+
1417
+ return true;
1418
+ } // verify_snapshot_integrity
1419
+
1420
+
1421
+ /**
1422
+ * Compares a selected snapshot with the current table set in DB
1423
+ *
1424
+ * @param string $uid Snapshot unique 6-char ID.
1425
+ *
1426
+ * @return string|WP_Error Formatted table with details on success, or error object on fail.
1427
+ */
1428
+ function do_compare_snapshots($uid) {
1429
+ global $wpdb;
1430
+ $tbl_core = $tbl_custom = 0;
1431
+ $current = $snapshot = array();
1432
+ $out = $out2 = $out3 = '';
1433
+
1434
+ if (($res = $this->verify_snapshot_integrity($uid)) !== true) {
1435
+ return $res;
1436
+ }
1437
+
1438
+ $table_status = $wpdb->get_results('SHOW TABLE STATUS');
1439
+ foreach ($table_status as $index => $table) {
1440
+ if (empty($table->Engine)) {
1441
+ continue;
1442
+ }
1443
+
1444
+ if (0 !== stripos($table->Name, $uid . '_') && 0 !== stripos($table->Name, $wpdb->prefix)) {
1445
+ continue;
1446
+ }
1447
+
1448
+ $info = array();
1449
+ $info['rows'] = $table->Rows;
1450
+ $info['size_data'] = $table->Data_length;
1451
+ $info['size_index'] = $table->Index_length;
1452
+ $schema = $wpdb->get_row('SHOW CREATE TABLE ' . $table->Name, ARRAY_N);
1453
+ $info['schema'] = $schema[1];
1454
+ $info['engine'] = $table->Engine;
1455
+ $info['fullname'] = $table->Name;
1456
+ $basename = str_replace(array($uid . '_'), array(''), $table->Name);
1457
+ $info['basename'] = $basename;
1458
+ $info['corename'] = str_replace(array($wpdb->prefix), array(''), $basename);
1459
+ $info['uid'] = $uid;
1460
+
1461
+ if (0 === stripos($table->Name, $uid . '_')) {
1462
+ $snapshot[$basename] = $info;
1463
+ }
1464
+
1465
+ if (0 === stripos($table->Name, $wpdb->prefix)) {
1466
+ $info['uid'] = '';
1467
+ $current[$basename] = $info;
1468
+ }
1469
+ } // foreach
1470
+
1471
+ $in_both = array_keys(array_intersect_key($current, $snapshot));
1472
+ $in_current_only = array_diff_key($current, $snapshot);
1473
+ $in_snapshot_only = array_diff_key($snapshot, $current);
1474
+
1475
+ $out .= '<br><br>';
1476
+ foreach ($in_current_only as $table) {
1477
+ $out .= '<div class="wpr-table-container in-current-only" data-table="' . $table['basename'] . '">';
1478
+ $out .= '<table>';
1479
+ $out .= '<tr title="Click to show/hide more info" class="wpr-table-missing header-row">';
1480
+ $out .= '<td><b>' . $table['fullname'] . '</b></td>';
1481
+ $out .= '<td>table is not present in snapshot<span class="dashicons dashicons-arrow-down-alt2"></span></td>';
1482
+ $out .= '</tr>';
1483
+ $out .= '<tr class="hidden">';
1484
+ $out .= '<td>';
1485
+ $out .= '<p>' . number_format($table['rows']) . ' row' . ($table['rows'] == 1? '': 's') . ' totaling ' . $this->format_size($table['size_data']) . ' in data and ' . $this->format_size($table['size_index']) . ' in index.</p>';
1486
+ $out .= '<pre>' . $table['schema'] . '</pre>';
1487
+ $out .= '</td>';
1488
+ $out .= '<td>&nbsp;</td>';
1489
+ $out .= '</tr>';
1490
+ $out .= '</table>';
1491
+ $out .= '</div>';
1492
+ } // foreach in current only
1493
+
1494
+ foreach ($in_snapshot_only as $table) {
1495
+ $out .= '<div class="wpr-table-container in-snapshot-only" data-table="' . $table['basename'] . '">';
1496
+ $out .= '<table>';
1497
+ $out .= '<tr title="Click to show/hide more info" class="wpr-table-missing header-row">';
1498
+ $out .= '<td>table is not present in current tables</td>';
1499
+ $out .= '<td><b>' . $table['fullname'] . '</b><span class="dashicons dashicons-arrow-down-alt2"></span></td>';
1500
+ $out .= '</tr>';
1501
+ $out .= '<tr class="hidden">';
1502
+ $out .= '<td>&nbsp;</td>';
1503
+ $out .= '<td>';
1504
+ $out .= '<p>' . number_format($table['rows']) . ' row' . ($table['rows'] == 1? '': 's') . ' totaling ' . $this->format_size($table['size_data']) . ' in data and ' . $this->format_size($table['size_index']) . ' in index.</p>';
1505
+ $out .= '<pre>' . $table['schema'] . '</pre>';
1506
+ $out .= '</td>';
1507
+ $out .= '</tr>';
1508
+ $out .= '</table>';
1509
+ $out .= '</div>';
1510
+ } // foreach in snapshot only
1511
+
1512
+ foreach ($in_both as $tablename) {
1513
+ $tbl_current = $current[$tablename];
1514
+ $tbl_snapshot = $snapshot[$tablename];
1515
+
1516
+ $schema1 = preg_replace('/(auto_increment=)([0-9]*) /i', '${1}1 ', $tbl_current['schema'], 1);
1517
+ $schema2 = preg_replace('/(auto_increment=)([0-9]*) /i', '${1}1 ', $tbl_snapshot['schema'], 1);
1518
+ $tbl_snapshot['tmp_schema'] = str_replace($tbl_snapshot['uid'] . '_' . $tablename, $tablename, $tbl_snapshot['schema']);
1519
+ $schema2 = str_replace($tbl_snapshot['uid'] . '_' . $tablename, $tablename, $schema2);
1520
+
1521
+ if ($tbl_current['rows'] == $tbl_snapshot['rows'] && $tbl_current['schema'] == $tbl_snapshot['tmp_schema']) {
1522
+ $out3 .= '<div class="wpr-table-container identical" data-table="' . $tablename . '">';
1523
+ $out3 .= '<table>';
1524
+ $out3 .= '<tr title="Click to show/hide more info" class="wpr-table-match header-row">';
1525
+ $out3 .= '<td><b>' . $tbl_current['fullname'] . '</b></td>';
1526
+ $out3 .= '<td><b>' . $tbl_snapshot['fullname'] . '</b><span class="dashicons dashicons-arrow-down-alt2"></span></td>';
1527
+ $out3 .= '</tr>';
1528
+ $out3 .= '<tr class="hidden">';
1529
+ $out3 .= '<td>';
1530
+ $out3 .= '<p>' . number_format($tbl_current['rows']) . ' rows totaling ' . $this->format_size($tbl_current['size_data']) . ' in data and ' . $this->format_size($tbl_current['size_index']) . ' in index.</p>';
1531
+ $out3 .= '<pre>' . $tbl_current['schema'] . '</pre>';
1532
+ $out3 .= '</td>';
1533
+ $out3 .= '<td>';
1534
+ $out3 .= '<p>' . number_format($tbl_snapshot['rows']) . ' rows totaling ' . $this->format_size($tbl_snapshot['size_data']) . ' in data and ' . $this->format_size($tbl_snapshot['size_index']) . ' in index.</p>';
1535
+ $out3 .= '<pre>' . $tbl_snapshot['schema'] . '</pre>';
1536
+ $out3 .= '</td>';
1537
+ $out3 .= '</tr>';
1538
+ $out3 .= '</table>';
1539
+ $out3 .= '</div>';
1540
+ } elseif ($schema1 != $schema2) {
1541
+ require_once $this->plugin_dir . 'libs/diff.php';
1542
+ require_once $this->plugin_dir . 'libs/diff/Renderer/Html/SideBySide.php';
1543
+ $diff = new Diff(explode("\n", $tbl_current['schema']), explode("\n", $tbl_snapshot['schema']), array('ignoreWhitespace' => false));
1544
+ $renderer = new Diff_Renderer_Html_SideBySide;
1545
+
1546
+ $out2 .= '<div class="wpr-table-container" data-table="' . $tbl_current['basename'] . '">';
1547
+ $out2 .= '<table>';
1548
+ $out2 .= '<tr title="Click to show/hide more info" class="wpr-table-difference header-row">';
1549
+ $out2 .= '<td><b>' . $tbl_current['fullname'] . '</b> table schemas do not match</td>';
1550
+ $out2 .= '<td><b>' . $tbl_snapshot['fullname'] . '</b> table schemas do not match<span class="dashicons dashicons-arrow-down-alt2"></span></td>';
1551
+ $out2 .= '</tr>';
1552
+ $out2 .= '<tr class="hidden">';
1553
+ $out2 .= '<td>';
1554
+ $out2 .= '<p>' . number_format($tbl_current['rows']) . ' rows totaling ' . $this->format_size($tbl_current['size_data']) . ' in data and ' . $this->format_size($tbl_current['size_index']) . ' in index.</p>';
1555
+ $out2 .= '</td>';
1556
+ $out2 .= '<td>';
1557
+ $out2 .= '<p>' . number_format($tbl_snapshot['rows']) . ' rows totaling ' . $this->format_size($tbl_snapshot['size_data']) . ' in data and ' . $this->format_size($tbl_snapshot['size_index']) . ' in index.</p>';
1558
+ $out2 .= '</td>';
1559
+ $out2 .= '</tr>';
1560
+ $out2 .= '<tr class="hidden">';
1561
+ $out2 .= '<td colspan="2" class="no-padding">';
1562
+ $out2 .= $diff->Render($renderer);
1563
+ $out2 .= '</td>';
1564
+ $out2 .= '</tr>';
1565
+ $out2 .= '</table>';
1566
+ $out2 .= '</div>';
1567
+ } else {
1568
+ $out2 .= '<div class="wpr-table-container" data-table="' . $tbl_current['basename'] . '">';
1569
+ $out2 .= '<table>';
1570
+ $out2 .= '<tr title="Click to show/hide more info" class="wpr-table-difference header-row">';
1571
+ $out2 .= '<td><b>' . $tbl_current['fullname'] . '</b> data in tables does not match</td>';
1572
+ $out2 .= '<td><b>' . $tbl_snapshot['fullname'] . '</b> data in tables does not match<span class="dashicons dashicons-arrow-down-alt2"></span></td>';
1573
+ $out2 .= '</tr>';
1574
+ $out2 .= '<tr class="hidden">';
1575
+ $out2 .= '<td>';
1576
+ $out2 .= '<p>' . number_format($tbl_current['rows']) . ' rows totaling ' . $this->format_size($tbl_current['size_data']) . ' in data and ' . $this->format_size($tbl_current['size_index']) . ' in index.</p>';
1577
+ $out2 .= '</td>';
1578
+ $out2 .= '<td>';
1579
+ $out2 .= '<p>' . number_format($tbl_snapshot['rows']) . ' rows totaling ' . $this->format_size($tbl_snapshot['size_data']) . ' in data and ' . $this->format_size($tbl_snapshot['size_index']) . ' in index.</p>';
1580
+ $out2 .= '</td>';
1581
+ $out2 .= '</tr>';
1582
+
1583
+ $out2 .= '<tr class="hidden">';
1584
+ $out2 .= '<td colspan="2">';
1585
+ if ($tbl_current['corename'] == 'options') {
1586
+ $ss_prefix = $tbl_snapshot['uid'] . '_' . $wpdb->prefix;
1587
+ $diff_rows = $wpdb->get_results("SELECT {$wpdb->prefix}options.option_name, {$wpdb->prefix}options.option_value AS current_value, {$ss_prefix}options.option_value AS snapshot_value FROM {$wpdb->prefix}options LEFT JOIN {$ss_prefix}options ON {$ss_prefix}options.option_name = {$wpdb->prefix}options.option_name WHERE {$wpdb->prefix}options.option_value != {$ss_prefix}options.option_value LIMIT 100;");
1588
+ $only_current = $wpdb->get_results("SELECT {$wpdb->prefix}options.option_name, {$wpdb->prefix}options.option_value AS current_value, {$ss_prefix}options.option_value AS snapshot_value FROM {$wpdb->prefix}options LEFT JOIN {$ss_prefix}options ON {$ss_prefix}options.option_name = {$wpdb->prefix}options.option_name WHERE {$ss_prefix}options.option_value IS NULL LIMIT 100;");
1589
+ $only_snapshot = $wpdb->get_results("SELECT {$wpdb->prefix}options.option_name, {$wpdb->prefix}options.option_value AS current_value, {$ss_prefix}options.option_value AS snapshot_value FROM {$wpdb->prefix}options LEFT JOIN {$ss_prefix}options ON {$ss_prefix}options.option_name = {$wpdb->prefix}options.option_name WHERE {$wpdb->prefix}options.option_value IS NULL LIMIT 100;");
1590
+ $out2 .= '<table class="table_diff">';
1591
+ $out2 .= '<tr><td style="width: 100px;"><b>Option Name</b></td><td><b>Current Value</b></td><td><b>Snapshot Value</b></td></tr>';
1592
+ foreach ($diff_rows as $row) {
1593
+ $out2 .= '<tr>';
1594
+ $out2 .= '<td style="width: 100px;">' . $row->option_name . '</td>';
1595
+ $out2 .= '<td>' . (empty($row->current_value)? '<i>empty</i>': $row->current_value) . '</td>';
1596
+ $out2 .= '<td>' . (empty($row->snapshot_value)? '<i>empty</i>': $row->snapshot_value) . '</td>';
1597
+ $out2 .= '</tr>';
1598
+ } // foreach
1599
+ foreach ($only_current as $row) {
1600
+ $out2 .= '<tr>';
1601
+ $out2 .= '<td style="width: 100px;">' . $row->option_name . '</td>';
1602
+ $out2 .= '<td>' . (empty($row->current_value)? '<i>empty</i>': $row->current_value) . '</td>';
1603
+ $out2 .= '<td><i>not found in snapshot</i></td>';
1604
+ $out2 .= '</tr>';
1605
+ } // foreach
1606
+ foreach ($only_current as $row) {
1607
+ $out2 .= '<tr>';
1608
+ $out2 .= '<td style="width: 100px;">' . $row->option_name . '</td>';
1609
+ $out2 .= '<td><i>not found in current tables</i></td>';
1610
+ $out2 .= '<td>' . (empty($row->snapshot_value)? '<i>empty</i>': $row->snapshot_value) . '</td>';
1611
+ $out2 .= '</tr>';
1612
+ } // foreach
1613
+ $out2 .= '</table>';
1614
+ } else {
1615
+ $out2 .= '<p class="textcenter">Detailed data diff is not available for this table.</p>';
1616
+ }
1617
+ $out2 .= '</td>';
1618
+ $out2 .= '</tr>';
1619
+
1620
+ $out2 .= '</table>';
1621
+ $out2 .= '</div>';
1622
+ }
1623
+ } // foreach in both
1624
+
1625
+ return $out . $out2 . $out3;
1626
+ } // do_compare_snapshots
1627
+
1628
+
1629
+ /**
1630
+ * Generates a unique 6-char snapshot ID; verified non-existing
1631
+ *
1632
+ * @return string
1633
+ */
1634
+ function generate_snapshot_uid() {
1635
+ global $wpdb;
1636
+ $snapshots = $this->get_snapshots();
1637
+ $cnt = 0;
1638
+ $uid = false;
1639
+
1640
+ do {
1641
+ $cnt++;
1642
+ $uid = sprintf('%06x', mt_rand(0, 0xFFFFFF));
1643
+
1644
+ $verify_db = $wpdb->get_col($wpdb->prepare('SHOW TABLES LIKE %s', array('%' . $uid . '%')));
1645
+ } while (!empty($verify_db) && isset($snapshots[$uid]) && $cnt < 30);
1646
+
1647
+ if ($cnt == 30) {
1648
+ $uid = false;
1649
+ }
1650
+
1651
+ return $uid;
1652
+ } // generate_snapshot_uid
1653
+
1654
+
1655
  /**
1656
  * Clean up on uninstall; no action on deactive at the moment
1657
  *
1659
  */
1660
  static function uninstall() {
1661
  delete_option('wp-reset');
1662
+ delete_option('wp-reset-snapshots');
1663
  } // uninstall
1664
 
1665