Changeset 3754379
- Timestamp:
- 10/19/2026 09:14:46 AM (3 weeks ago)
- Location:
- vtables/trunk
- Files:
-
- 158 added
- 24 deleted
- 21 edited
-
README.md (deleted)
-
composer.json (added)
-
composer.lock (added)
-
dist/spa/assets/DashboardPage.6fd5dbbd.css (deleted)
-
dist/spa/assets/DashboardPage.a83e5795.js (deleted)
-
dist/spa/assets/DashboardTable.1de870d3.js (deleted)
-
dist/spa/assets/DashboardTable.b862b64c.css (deleted)
-
dist/spa/assets/ErrorNotFound.9383f271.js (deleted)
-
dist/spa/assets/ErrorNotFound.a39cffbf.js (added)
-
dist/spa/assets/IndexPage.95348cb2.js (added)
-
dist/spa/assets/IndexPage.c7315fe1.css (added)
-
dist/spa/assets/LogsPage.69670a09.js (added)
-
dist/spa/assets/MainLayout.2f9e91b7.js (added)
-
dist/spa/assets/MainLayout.74be7155.js (deleted)
-
dist/spa/assets/ProFeatureTeaser.9d026ba3.css (added)
-
dist/spa/assets/ProFeatureTeaser.fa10d20f.js (added)
-
dist/spa/assets/QItemSection.12d1f83c.js (added)
-
dist/spa/assets/QResizeObserver.68b7499a.js (deleted)
-
dist/spa/assets/QResizeObserver.a484717f.js (added)
-
dist/spa/assets/QScrollObserver.2143d593.js (deleted)
-
dist/spa/assets/TablePage.0f2ac121.js (added)
-
dist/spa/assets/TablePage.1bc519b9.css (added)
-
dist/spa/assets/axios.8493cb2c.js (deleted)
-
dist/spa/assets/axios.af5a39b4.js (added)
-
dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.fd84f88b.woff (added)
-
dist/spa/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.83be7b2f.woff2 (added)
-
dist/spa/assets/i18n.b8837d2d.js (deleted)
-
dist/spa/assets/i18n.e8376384.js (added)
-
dist/spa/assets/index.64481c02.css (added)
-
dist/spa/assets/index.88166ef6.js (added)
-
dist/spa/assets/index.92adfbea.css (deleted)
-
dist/spa/assets/index.ad4a434c.js (deleted)
-
dist/spa/assets/index.b9370b11.js (added)
-
dist/spa/assets/index.fd5ac4ef.js (deleted)
-
dist/spa/assets/services.774084a4.js (added)
-
dist/spa/assets/services.7fb50a72.js (deleted)
-
dist/spa/assets/sizeHelper.6af7a53e.js (deleted)
-
dist/spa/assets/vue-i18n.runtime.esm-bundler.a72a2444.js (added)
-
dist/spa/assets/vue-i18n.runtime.esm-bundler.f35c74df.js (deleted)
-
dist/spa/assets/wpapi.3145640b.js (added)
-
dist/spa/assets/wpapi.af2b6423.js (deleted)
-
dist/spa/css/quasar.prod.css (deleted)
-
dist/spa/css/vtables.css (modified) (3 diffs)
-
dist/spa/index.html (modified) (1 diff)
-
dist/spa/js (deleted)
-
includes/Classes/Activator.php (modified) (7 diffs)
-
includes/Classes/Embed.php (added)
-
includes/Classes/LoadAssets.php (modified) (1 diff)
-
includes/Classes/index.php (added)
-
includes/autoload.php (modified) (2 diffs)
-
includes/compat.php (added)
-
includes/endpoints.php (modified) (2 diffs)
-
includes/migrate-rows.php (added)
-
includes/settings-policy.json (added)
-
includes/settings-policy.php (added)
-
includes/shortcode.html (modified) (1 diff)
-
index.html (modified) (1 diff)
-
package-lock.json (modified) (47 diffs)
-
package.json (modified) (4 diffs)
-
public/css/quasar.prod.css (deleted)
-
public/css/vtables.css (modified) (3 diffs)
-
public/js (deleted)
-
quasar.config.js (modified) (4 diffs)
-
readme.txt (modified) (1 diff)
-
scripts (added)
-
scripts/patch-asset-base.mjs (added)
-
src/App.vue (added)
-
src/boot/i18n.ts (modified) (2 diffs)
-
src/boot/services.ts (modified) (1 diff)
-
src/boot/wpapi.ts (modified) (1 diff)
-
src/components (added)
-
src/components/ImportProgressRow.vue (added)
-
src/components/ProFeatureTeaser.vue (added)
-
src/components/TableList.vue (added)
-
src/components/TableListUploader.vue (added)
-
src/components/TableListUploaderCreate.vue (added)
-
src/components/TableListUploaderUpload.vue (added)
-
src/components/TheTable.vue (added)
-
src/components/TheTableActions.vue (added)
-
src/components/TheTableActionsRename.vue (added)
-
src/components/TheTableActionsShortcode.vue (added)
-
src/components/TheTableCell.vue (added)
-
src/components/TheTableSettings.vue (added)
-
src/components/TheTableSettingsDimensions.vue (added)
-
src/components/TheTableSettingsGeneral.vue (added)
-
src/components/TheTableSettingsInput.vue (added)
-
src/components/TheTableSettingsReset.vue (added)
-
src/components/TheTableSettingsSelect.vue (added)
-
src/components/TheTableSettingsSelectColor.vue (added)
-
src/components/TheTableSettingsToggle.vue (added)
-
src/components/TheTableSettingsTop.vue (added)
-
src/composables (added)
-
src/composables/tableCellContext.ts (added)
-
src/composables/useCellRendering.ts (added)
-
src/composables/useColumnRowManagement.ts (added)
-
src/composables/useInlineCellEdit.ts (added)
-
src/composables/usePendingRowChanges.ts (added)
-
src/composables/useServerRows.ts (added)
-
src/composables/useTableActions.ts (added)
-
src/composables/useTableDataFetch.ts (added)
-
src/composables/useTableExport.ts (added)
-
src/composables/useTableStyles.ts (added)
-
src/css/app.scss (modified) (1 diff)
-
src/domains (deleted)
-
src/helpers (added)
-
src/helpers/assignTableRows.ts (added)
-
src/helpers/columnsHelper.ts (added)
-
src/helpers/formatDetectHelper.ts (added)
-
src/helpers/promiseHelper.ts (added)
-
src/helpers/settingsAppliedHelper.ts (added)
-
src/helpers/settingsPolicyHelper.ts (added)
-
src/helpers/settingsSaveHelper.ts (added)
-
src/helpers/sizeHelper.ts (added)
-
src/i18n/de (added)
-
src/i18n/de/index.ts (added)
-
src/i18n/en-US/index.ts (modified) (16 diffs)
-
src/i18n/es (added)
-
src/i18n/es/index.ts (added)
-
src/i18n/fr (added)
-
src/i18n/fr/index.ts (added)
-
src/i18n/hi (added)
-
src/i18n/hi/index.ts (added)
-
src/i18n/index.ts (modified) (1 diff)
-
src/i18n/it (added)
-
src/i18n/it/index.ts (added)
-
src/i18n/nb (added)
-
src/i18n/nb/index.ts (added)
-
src/i18n/pt (added)
-
src/i18n/pt/index.ts (added)
-
src/i18n/ru (added)
-
src/i18n/ru/index.ts (added)
-
src/i18n/sv (added)
-
src/i18n/sv/index.ts (added)
-
src/i18n/tr (added)
-
src/i18n/tr/index.ts (added)
-
src/i18n/zh-CN (added)
-
src/i18n/zh-CN/index.ts (added)
-
src/i18n/zh-HK (added)
-
src/i18n/zh-HK/index.ts (added)
-
src/layouts (added)
-
src/layouts/MainLayout.vue (added)
-
src/pages (added)
-
src/pages/ErrorNotFound.vue (added)
-
src/pages/IndexPage.vue (added)
-
src/pages/LogsPage.vue (added)
-
src/pages/TablePage.vue (added)
-
src/router (added)
-
src/router/index.ts (added)
-
src/router/routes.ts (added)
-
src/services (added)
-
src/services/index.ts (added)
-
src/stores (added)
-
src/stores/index.ts (added)
-
src/stores/main (added)
-
src/stores/main/actions.ts (added)
-
src/stores/main/constants.ts (added)
-
src/stores/main/index.ts (added)
-
src/stores/main/models.ts (added)
-
src/stores/main/state.ts (added)
-
src/stores/store-flag.d.ts (added)
-
src/stores/uploads (added)
-
src/stores/uploads/actions.ts (added)
-
src/stores/uploads/index.ts (added)
-
src/stores/uploads/state.ts (added)
-
uninstall.php (modified) (1 diff)
-
vendor (added)
-
vendor/php (added)
-
vendor/php/autoload.php (added)
-
vendor/php/composer (added)
-
vendor/php/composer/ClassLoader.php (added)
-
vendor/php/composer/InstalledVersions.php (added)
-
vendor/php/composer/LICENSE (added)
-
vendor/php/composer/autoload_classmap.php (added)
-
vendor/php/composer/autoload_namespaces.php (added)
-
vendor/php/composer/autoload_psr4.php (added)
-
vendor/php/composer/autoload_real.php (added)
-
vendor/php/composer/autoload_static.php (added)
-
vendor/php/composer/installed.json (added)
-
vendor/php/composer/installed.php (added)
-
vendor/php/composer/platform_check.php (added)
-
vendor/php/shuchkin (added)
-
vendor/php/shuchkin/simplexls (added)
-
vendor/php/shuchkin/simplexls/.gitattributes (added)
-
vendor/php/shuchkin/simplexls/LICENSE (added)
-
vendor/php/shuchkin/simplexls/README.md (added)
-
vendor/php/shuchkin/simplexls/composer.json (added)
-
vendor/php/shuchkin/simplexls/examples (added)
-
vendor/php/shuchkin/simplexls/examples/01-basic_usage.php (added)
-
vendor/php/shuchkin/simplexls/examples/02-rows_with_header_values_as_keys.php (added)
-
vendor/php/shuchkin/simplexls/examples/books.xls (added)
-
vendor/php/shuchkin/simplexls/license.md (added)
-
vendor/php/shuchkin/simplexls/src (added)
-
vendor/php/shuchkin/simplexls/src/SimpleXLS.php (added)
-
vendor/php/shuchkin/simplexlsx (added)
-
vendor/php/shuchkin/simplexlsx/CHANGELOG.md (added)
-
vendor/php/shuchkin/simplexlsx/README.md (added)
-
vendor/php/shuchkin/simplexlsx/composer.json (added)
-
vendor/php/shuchkin/simplexlsx/license.md (added)
-
vendor/php/shuchkin/simplexlsx/src (added)
-
vendor/php/shuchkin/simplexlsx/src/SimpleXLSX.php (added)
-
vendor/php/shuchkin/simplexlsx/src/SimpleXLSXEx.php (added)
-
vtables.php (modified) (6 diffs)
-
yarn.lock (deleted)
Legend:
- Unmodified
- Added
- Removed
-
vtables/trunk/dist/spa/css/vtables.css
r3576926 r3754379 1 :root { 2 --red: #f44336; 3 --red-1: #ffebee; 4 --red-2: #ffcdd2; 5 --red-3: #ef9a9a; 6 --red-4: #e57373; 7 --red-5: #ef5350; 8 --red-6: #f44336; 9 --red-7: #e53935; 10 --red-8: #d32f2f; 11 --red-9: #c62828; 12 --red-10: #b71c1c; 13 --red-11: #ff8a80; 14 --red-12: #ff5252; 15 --red-13: #ff1744; 16 --red-14: #d50000; 17 --pink: #e91e63; 18 --pink-1: #fce4ec; 19 --pink-2: #f8bbd0; 20 --pink-3: #f48fb1; 21 --pink-4: #f06292; 22 --pink-5: #ec407a; 23 --pink-6: #e91e63; 24 --pink-7: #d81b60; 25 --pink-8: #c2185b; 26 --pink-9: #ad1457; 27 --pink-10: #880e4f; 28 --pink-11: #ff80ab; 29 --pink-12: #ff4081; 30 --pink-13: #f50057; 31 --pink-14: #c51162; 32 --purple: #9c27b0; 33 --purple-1: #f3e5f5; 34 --purple-2: #e1bee7; 35 --purple-3: #ce93d8; 36 --purple-4: #ba68c8; 37 --purple-5: #ab47bc; 38 --purple-6: #9c27b0; 39 --purple-7: #8e24aa; 40 --purple-8: #7b1fa2; 41 --purple-9: #6a1b9a; 42 --purple-10: #4a148c; 43 --purple-11: #ea80fc; 44 --purple-12: #e040fb; 45 --purple-13: #d500f9; 46 --purple-14: #aa00ff; 47 --deep-purple: #673ab7; 48 --deep-purple-1: #ede7f6; 49 --deep-purple-2: #d1c4e9; 50 --deep-purple-3: #b39ddb; 51 --deep-purple-4: #9575cd; 52 --deep-purple-5: #7e57c2; 53 --deep-purple-6: #673ab7; 54 --deep-purple-7: #5e35b1; 55 --deep-purple-8: #512da8; 56 --deep-purple-9: #4527a0; 57 --deep-purple-10: #311b92; 58 --deep-purple-11: #b388ff; 59 --deep-purple-12: #7c4dff; 60 --deep-purple-13: #651fff; 61 --deep-purple-14: #6200ea; 62 --indigo: #3f51b5; 63 --indigo-1: #e8eaf6; 64 --indigo-2: #c5cae9; 65 --indigo-3: #9fa8da; 66 --indigo-4: #7986cb; 67 --indigo-5: #5c6bc0; 68 --indigo-6: #3f51b5; 69 --indigo-7: #3949ab; 70 --indigo-8: #303f9f; 71 --indigo-9: #283593; 72 --indigo-10: #1a237e; 73 --indigo-11: #8c9eff; 74 --indigo-12: #536dfe; 75 --indigo-13: #3d5afe; 76 --indigo-14: #304ffe; 77 --blue: #2196f3; 78 --blue-1: #e3f2fd; 79 --blue-2: #bbdefb; 80 --blue-3: #90caf9; 81 --blue-4: #64b5f6; 82 --blue-5: #42a5f5; 83 --blue-6: #2196f3; 84 --blue-7: #1e88e5; 85 --blue-8: #1976d2; 86 --blue-9: #1565c0; 87 --blue-10: #0d47a1; 88 --blue-11: #82b1ff; 89 --blue-12: #448aff; 90 --blue-13: #2979ff; 91 --blue-14: #2962ff; 92 --light-blue: #03a9f4; 93 --light-blue-1: #e1f5fe; 94 --light-blue-2: #b3e5fc; 95 --light-blue-3: #81d4fa; 96 --light-blue-4: #4fc3f7; 97 --light-blue-5: #29b6f6; 98 --light-blue-6: #03a9f4; 99 --light-blue-7: #039be5; 100 --light-blue-8: #0288d1; 101 --light-blue-9: #0277bd; 102 --light-blue-10: #01579b; 103 --light-blue-11: #80d8ff; 104 --light-blue-12: #40c4ff; 105 --light-blue-13: #00b0ff; 106 --light-blue-14: #0091ea; 107 --cyan: #00bcd4; 108 --cyan-1: #e0f7fa; 109 --cyan-2: #b2ebf2; 110 --cyan-3: #80deea; 111 --cyan-4: #4dd0e1; 112 --cyan-5: #26c6da; 113 --cyan-6: #00bcd4; 114 --cyan-7: #00acc1; 115 --cyan-8: #0097a7; 116 --cyan-9: #00838f; 117 --cyan-10: #006064; 118 --cyan-11: #84ffff; 119 --cyan-12: #18ffff; 120 --cyan-13: #00e5ff; 121 --cyan-14: #00b8d4; 122 --teal: #009688; 123 --teal-1: #e0f2f1; 124 --teal-2: #b2dfdb; 125 --teal-3: #80cbc4; 126 --teal-4: #4db6ac; 127 --teal-5: #26a69a; 128 --teal-6: #009688; 129 --teal-7: #00897b; 130 --teal-8: #00796b; 131 --teal-9: #00695c; 132 --teal-10: #004d40; 133 --teal-11: #a7ffeb; 134 --teal-12: #64ffda; 135 --teal-13: #1de9b6; 136 --teal-14: #00bfa5; 137 --green: #4caf50; 138 --green-1: #e8f5e9; 139 --green-2: #c8e6c9; 140 --green-3: #a5d6a7; 141 --green-4: #81c784; 142 --green-5: #66bb6a; 143 --green-6: #4caf50; 144 --green-7: #43a047; 145 --green-8: #388e3c; 146 --green-9: #2e7d32; 147 --green-10: #1b5e20; 148 --green-11: #b9f6ca; 149 --green-12: #69f0ae; 150 --green-13: #00e676; 151 --green-14: #00c853; 152 --light-green: #8bc34a; 153 --light-green-1: #f1f8e9; 154 --light-green-2: #dcedc8; 155 --light-green-3: #c5e1a5; 156 --light-green-4: #aed581; 157 --light-green-5: #9ccc65; 158 --light-green-6: #8bc34a; 159 --light-green-7: #7cb342; 160 --light-green-8: #689f38; 161 --light-green-9: #558b2f; 162 --light-green-10: #33691e; 163 --light-green-11: #ccff90; 164 --light-green-12: #b2ff59; 165 --light-green-13: #76ff03; 166 --light-green-14: #64dd17; 167 --lime: #cddc39; 168 --lime-1: #f9fbe7; 169 --lime-2: #f0f4c3; 170 --lime-3: #e6ee9c; 171 --lime-4: #dce775; 172 --lime-5: #d4e157; 173 --lime-6: #cddc39; 174 --lime-7: #c0ca33; 175 --lime-8: #afb42b; 176 --lime-9: #9e9d24; 177 --lime-10: #827717; 178 --lime-11: #f4ff81; 179 --lime-12: #eeff41; 180 --lime-13: #c6ff00; 181 --lime-14: #aeea00; 182 --yellow: #ffeb3b; 183 --yellow-1: #fffde7; 184 --yellow-2: #fff9c4; 185 --yellow-3: #fff59d; 186 --yellow-4: #fff176; 187 --yellow-5: #ffee58; 188 --yellow-6: #ffeb3b; 189 --yellow-7: #fdd835; 190 --yellow-8: #fbc02d; 191 --yellow-9: #f9a825; 192 --yellow-10: #f57f17; 193 --yellow-11: #ffff8d; 194 --yellow-12: #ffff00; 195 --yellow-13: #ffea00; 196 --yellow-14: #ffd600; 197 --amber: #ffc107; 198 --amber-1: #fff8e1; 199 --amber-2: #ffecb3; 200 --amber-3: #ffe082; 201 --amber-4: #ffd54f; 202 --amber-5: #ffca28; 203 --amber-6: #ffc107; 204 --amber-7: #ffb300; 205 --amber-8: #ffa000; 206 --amber-9: #ff8f00; 207 --amber-10: #ff6f00; 208 --amber-11: #ffe57f; 209 --amber-12: #ffd740; 210 --amber-13: #ffc400; 211 --amber-14: #ffab00; 212 --orange: #ff9800; 213 --orange-1: #fff3e0; 214 --orange-2: #ffe0b2; 215 --orange-3: #ffcc80; 216 --orange-4: #ffb74d; 217 --orange-5: #ffa726; 218 --orange-6: #ff9800; 219 --orange-7: #fb8c00; 220 --orange-8: #f57c00; 221 --orange-9: #ef6c00; 222 --orange-10: #e65100; 223 --orange-11: #ffd180; 224 --orange-12: #ffab40; 225 --orange-13: #ff9100; 226 --orange-14: #ff6d00; 227 --deep-orange: #ff5722; 228 --deep-orange-1: #fbe9e7; 229 --deep-orange-2: #ffccbc; 230 --deep-orange-3: #ffab91; 231 --deep-orange-4: #ff8a65; 232 --deep-orange-5: #ff7043; 233 --deep-orange-6: #ff5722; 234 --deep-orange-7: #f4511e; 235 --deep-orange-8: #e64a19; 236 --deep-orange-9: #d84315; 237 --deep-orange-10: #bf360c; 238 --deep-orange-11: #ff9e80; 239 --deep-orange-12: #ff6e40; 240 --deep-orange-13: #ff3d00; 241 --deep-orange-14: #dd2c00; 242 --brown: #795548; 243 --brown-1: #efebe9; 244 --brown-2: #d7ccc8; 245 --brown-3: #bcaaa4; 246 --brown-4: #a1887f; 247 --brown-5: #8d6e63; 248 --brown-6: #795548; 249 --brown-7: #6d4c41; 250 --brown-8: #5d4037; 251 --brown-9: #4e342e; 252 --brown-10: #3e2723; 253 --brown-11: #d7ccc8; 254 --brown-12: #bcaaa4; 255 --brown-13: #8d6e63; 256 --brown-14: #5d4037; 257 --grey: #9e9e9e; 258 --grey-1: #fafafa; 259 --grey-2: #f5f5f5; 260 --grey-3: #eeeeee; 261 --grey-4: #e0e0e0; 262 --grey-5: #bdbdbd; 263 --grey-6: #9e9e9e; 264 --grey-7: #757575; 265 --grey-8: #616161; 266 --grey-9: #424242; 267 --grey-10: #212121; 268 --grey-11: #f5f5f5; 269 --grey-12: #eeeeee; 270 --grey-13: #bdbdbd; 271 --grey-14: #616161; 272 --blue-grey: #607d8b; 273 --blue-grey-1: #eceff1; 274 --blue-grey-2: #cfd8dc; 275 --blue-grey-3: #b0bec5; 276 --blue-grey-4: #90a4ae; 277 --blue-grey-5: #78909c; 278 --blue-grey-6: #607d8b; 279 --blue-grey-7: #546e7a; 280 --blue-grey-8: #455a64; 281 --blue-grey-9: #37474f; 282 --blue-grey-10: #263238; 283 --blue-grey-11: #cfd8dc; 284 --blue-grey-12: #b0bec5; 285 --blue-grey-13: #78909c; 286 --blue-grey-14: #455a64; 287 } 288 289 body.q-body--prevent-scroll { 290 position: initial !important; 291 } 292 1 293 #wpcontent { 2 294 padding: 20px; … … 5 297 .gap-5 { 6 298 gap: 5px; 299 } 300 301 .gap-10 { 302 gap: 10px; 7 303 } 8 304 … … 40 336 } 41 337 42 #adminmenu #toplevel_page_vtables .wp-menu-image img { 43 opacity: 1; 44 } 338 .q-table td, 339 .q-table th { 340 white-space: normal; 341 word-wrap: break-word; 342 } 343 344 #q-notify .row { 345 padding-top: 0; 346 padding-bottom: 0; 347 } 348 349 /* ===== TheTable styles ===== */ 350 351 .q-item__section--avatar { 352 min-width: unset; 353 } 354 355 .settings-open { 356 margin-right: 325px; 357 } 358 359 .fullscreen .q-table__top { 360 padding: 0; 361 } 362 363 .fullscreen .q-table__top .vtable__top { 364 padding: 12px 16px; 365 } 366 367 .fullscreen.q-table--dense .vtable__top { 368 padding: 8px 16px; 369 } 370 371 .cell-value { 372 display: block; 373 white-space: initial; 374 width: inherit; 375 } 376 377 .q-table--cell-separator .q-table__top { 378 border-bottom: none !important; 379 } 380 381 .q-table--cell-separator .q-table__top .vtable__top { 382 border-bottom: 1px solid rgba(0, 0, 0, 0.12); 383 } 384 385 .vtable { 386 margin: 10px 0; 387 } 388 389 .vtable .q-table__bottom { 390 padding: 0; 391 } 392 393 .vtable .q-table__bottom > div { 394 border-radius: 0 0 4px 4px; 395 padding: 4px 10px 4px 10px; 396 min-height: inherit; 397 } 398 399 .vtable .q-table__bottom--nodata { 400 padding-left: 10px; 401 padding-right: 10px; 402 } 403 404 .vtable .q-field__bottom { 405 padding-left: 0; 406 padding-right: 0; 407 position: absolute; 408 background: #fff; 409 } 410 411 .vtable .q-table__top { 412 padding: 0; 413 overflow: hidden; 414 } 415 416 .vtable .q-table__top > div { 417 padding: 12px 16px; 418 min-height: inherit; 419 } 420 421 .vtable .q-table--dense .q-table__top { 422 padding: 0; 423 overflow: hidden; 424 } 425 426 .vtable .q-table--dense .q-table__top > div { 427 padding: 8px 16px; 428 min-height: inherit; 429 } 430 431 .edit-icon { 432 position: absolute; 433 } 434 .q-td .edit-icon { 435 top: -8px; 436 } 437 /* Quasar makes body cells positioned, header cells not — the edit icon needs 438 the th as its containing block. */ 439 .q-table thead th { 440 position: relative; 441 } 442 .q-table thead th .edit-icon { 443 top: -6px; 444 } 445 .vtable__top h1 { 446 display: inline-block; 447 position: relative; 448 } 449 .vtable__top h1 .edit-icon { 450 top: -10px; 451 right: -45px; 452 } 453 .vtable__top:hover .edit-icon, 454 .q-td:hover .edit-icon, 455 .q-table thead th:hover .edit-icon, 456 .q-item__section:hover .edit-icon { 457 display: block !important; 458 } 459 460 .admin-bar-presented-on-fullscreen { 461 margin-top: 30px; 462 margin-left: 160px; 463 } 464 465 .fullscreen-btn { 466 height: 40px; 467 width: 40px; 468 } 469 470 .v-enter-active, 471 .v-leave-active { 472 transition: opacity 0.5s ease; 473 } 474 475 .v-enter-from, 476 .v-leave-to { 477 opacity: 0; 478 } 479 480 .q-table th.sortable { 481 cursor: pointer; 482 min-width: 116px; 483 } 484 485 .cell-raw-html > * { 486 cursor: pointer; 487 } 488 489 .q-table__container { 490 margin: 2px; 491 } 492 493 .pre-wrap { 494 white-space: pre-wrap; 495 } 496 497 .flex-no-wrap { 498 flex-wrap: nowrap; 499 } -
vtables/trunk/dist/spa/index.html
r3576926 r3754379 1 <!DOCTYPE html><html><head><title>vTables</title><meta charset=utf-8><meta name=description content=" Create stunning, interactive tables effortlessly. No coding required. The ultimate WordPress plugin, lets you design powerful tables in a few clicks. Transform your data visualization now."><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"> <script type="module" crossorigin src="/./wp-content/plugins/vtables/dist/spa/assets/index.fd5ac4ef.js"></script>2 <link rel="stylesheet" href=" /./wp-content/plugins/vtables/dist/spa/assets/index.92adfbea.css">1 <!DOCTYPE html><html><head><title>vTables</title><meta charset=utf-8><meta name=description content="Build interactive data tables from CSV, Excel and JSON files, or from scratch, and embed them anywhere with a shortcode."><meta name=format-detection content="telephone=no"><meta name=msapplication-tap-highlight content=no><meta name=viewport content="user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1,width=device-width"> <script type="module" crossorigin src="./assets/index.88166ef6.js"></script> 2 <link rel="stylesheet" href="./assets/index.64481c02.css"> 3 3 </head><body><div id=q-app></div></body></html> -
vtables/trunk/includes/Classes/Activator.php
r3133736 r3754379 7 7 /** 8 8 * ACTIVATOR CLASS 9 * @since 1.0.0 9 * 10 * Owns the schema shared with vTables Pro: wp_vtables (one row per table) and 11 * wp_vtables_source (one row per data row). Definitions are kept identical to 12 * Pro's Activator so either plugin can create or upgrade them and the other 13 * keeps working on the result. Pro-only tables (the WooCommerce product 14 * index) are left to Pro. Every step is idempotent. 10 15 */ 11 16 class Activator { … … 15 20 */ 16 21 public function vtables_migrate_databases($network_wide = false) { 17 global $wpdb; 18 if ($network_wide) { 19 // Retrieve all site IDs from this network (WordPress >= 4.6 provides easy to use functions for that). 20 if (function_exists('get_sites') && function_exists('get_current_network_id')) { 21 $site_ids = get_sites(array('fields' => 'ids', 'network_id' => get_current_network_id())); 22 } else { 23 $site_ids = $wpdb->get_col("SELECT blog_id FROM $wpdb->blogs WHERE site_id = $wpdb->siteid;"); 24 } 25 // Install the plugin for all these sites. 22 if ($network_wide && is_multisite()) { 23 $site_ids = get_sites(array('fields' => 'ids', 'network_id' => get_current_network_id())); 26 24 foreach ($site_ids as $site_id) { 27 25 switch_to_blog($site_id); … … 38 36 */ 39 37 private function vtables_migrate() { 40 $this->vtables_create_main_table(); 38 $this->vtables_create_main_table(); 39 $this->vtables_create_source_table(); 40 $this->vtables_update_table_type_values(); 41 41 } 42 42 43 43 /** 44 44 * CREATE MAIN TABLE 45 * 0.1.x/0.2.x created wp_vtables without table_type, woocommerce_categories 46 * and plan; they are added in place. 'plan' is NOT backfilled here: rows 47 * that predate it stay NULL so vTables Pro can grandfather them by their 48 * source type and license (vtables_pro_backfill_table_plans). Tables 49 * created by this plugin are stamped 'free' on insert. 45 50 */ 46 51 public function vtables_create_main_table() { … … 51 56 id int(10) NOT NULL AUTO_INCREMENT, 52 57 title text NULL DEFAULT NULL, 58 table_type text NULL DEFAULT NULL, 59 woocommerce_categories JSON, 53 60 settings json NULL DEFAULT NULL, 54 61 file_size text NULL DEFAULT NULL, … … 57 64 updated_at timestamp NULL DEFAULT CURRENT_TIMESTAMP, 58 65 source longtext NULL DEFAULT NULL, 66 plan text NULL DEFAULT NULL, 59 67 PRIMARY KEY (id) 60 ) $charset_collate;";68 ) $charset_collate;"; 61 69 62 70 $this->vtables_run_sql($sql, $table_name); 63 71 $this->vtables_add_new_column($table_name, 'table_type', 'TEXT'); 64 72 $this->vtables_add_new_column($table_name, 'woocommerce_categories', 'JSON'); 73 $this->vtables_add_new_column($table_name, 'plan', 'TEXT'); 74 } 75 76 /** 77 * CREATE SOURCE TABLE 78 * One row per imported data row, keyed by table_id, so large datasets stay 79 * out of the single wp_vtables.source longtext cell (and under the 80 * max_allowed_packet ceiling). 81 */ 82 public function vtables_create_source_table() { 83 global $wpdb; 84 $charset_collate = $wpdb->get_charset_collate(); 85 $table_name = $wpdb->prefix . 'vtables_source'; 86 $sql = "CREATE TABLE $table_name ( 87 id bigint(20) unsigned NOT NULL AUTO_INCREMENT, 88 table_id int(10) NOT NULL, 89 row_index int(10) unsigned NOT NULL, 90 row_json longtext NULL DEFAULT NULL, 91 search_blob longtext NULL DEFAULT NULL, 92 PRIMARY KEY (id), 93 KEY table_id_row_index (table_id, row_index) 94 ) $charset_collate;"; 95 96 $this->vtables_run_sql($sql, $table_name); 97 98 // For tables created by early vTables Pro 0.3.x, before search_blob existed. 99 $this->vtables_add_new_column($table_name, 'search_blob', 'TEXT'); 100 101 $this->vtables_upgrade_source_indexes($table_name); 102 } 103 104 /** 105 * UPGRADE SOURCE INDEX SLOTS 106 * 5 typed "slot" sub-columns (text + numeric) with composite indexes, plus 107 * a FULLTEXT index on search_blob. This plugin writes search_blob and the 108 * slot values on every row write (vtables_replace_source_rows), so the rows 109 * are ready for vTables Pro's server-side search/sort without a reindex. 110 */ 111 private function vtables_upgrade_source_indexes($table_name) { 112 for ($k = 1; $k <= 5; $k++) { 113 $this->vtables_add_typed_column($table_name, "idx_{$k}_txt", 'VARCHAR(191) NULL DEFAULT NULL'); 114 $this->vtables_add_typed_column($table_name, "idx_{$k}_num", 'DECIMAL(30,10) NULL DEFAULT NULL'); 115 } 116 117 for ($k = 1; $k <= 5; $k++) { 118 $this->vtables_add_index($table_name, "idx_{$k}_txt", "(table_id, idx_{$k}_txt)"); 119 $this->vtables_add_index($table_name, "idx_{$k}_num", "(table_id, idx_{$k}_num)"); 120 } 121 122 $this->vtables_add_fulltext_index($table_name, 'ft_search_blob', 'search_blob'); 123 } 124 125 /** 126 * UPDATE LEGACY TABLE_TYPE VALUES 127 * 0.1.0–0.1.4 stored 'local' (now 'csv') and 'remote' (now 128 * 'google_sheets'). The frontend and vTables Pro only know the current names. 129 */ 130 private function vtables_update_table_type_values() { 131 global $wpdb; 132 133 $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 134 "UPDATE {$wpdb->prefix}vtables SET table_type = %s WHERE table_type = %s", 135 'csv', 136 'local' 137 )); 138 139 $wpdb->query($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 140 "UPDATE {$wpdb->prefix}vtables SET table_type = %s WHERE table_type = %s", 141 'google_sheets', 142 'remote' 143 )); 144 } 145 146 /** 147 * ADD TYPED COLUMN 148 * maybe_add_column() only takes the column name, so we guard with SHOW 149 * COLUMNS ourselves to keep DECIMAL/VARCHAR definitions intact. 150 */ 151 private function vtables_add_typed_column($table_name, $column, $definition) { 152 global $wpdb; 153 $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 154 $wpdb->prepare('SHOW COLUMNS FROM `' . esc_sql($table_name) . '` LIKE %s', $wpdb->esc_like($column)) 155 ); 156 if ($exists) { 157 return; 158 } 159 $wpdb->query("ALTER TABLE `$table_name` ADD `$column` $definition"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL; the table, column and definition are fixed values from vtables_upgrade_source_indexes() 160 } 161 162 /** 163 * ADD INDEX (idempotent) 164 */ 165 private function vtables_add_index($table_name, $index_name, $columns_sql) { 166 global $wpdb; 167 $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 168 $wpdb->prepare('SHOW INDEX FROM `' . esc_sql($table_name) . '` WHERE Key_name = %s', $index_name) 169 ); 170 if ($exists) { 171 return; 172 } 173 $wpdb->query("ALTER TABLE `$table_name` ADD KEY `$index_name` $columns_sql"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL; the table, index and columns are fixed values from vtables_upgrade_source_indexes() 174 } 175 176 /** 177 * ADD FULLTEXT INDEX (idempotent) 178 * May fail on very old / non-InnoDB engines; the rest of the migration 179 * still completes (LIKE search remains the correctness path). 180 */ 181 private function vtables_add_fulltext_index($table_name, $index_name, $column) { 182 global $wpdb; 183 $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 184 $wpdb->prepare('SHOW INDEX FROM `' . esc_sql($table_name) . '` WHERE Key_name = %s', $index_name) 185 ); 186 if ($exists) { 187 return; 188 } 189 $wpdb->query("ALTER TABLE `$table_name` ADD FULLTEXT KEY `$index_name` (`$column`)"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.SchemaChange, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DDL; the table, index and column are fixed values from vtables_upgrade_source_indexes() 65 190 } 66 191 … … 68 193 * RUN SQL 69 194 */ 70 private function vtables_run_sql($sql, $table Name) {71 global $wpdb; 72 if ($wpdb->get_var( "SHOW TABLES LIKE '$tableName'") != $tableName) {73 require_once (ABSPATH . 'wp-admin/includes/upgrade.php');195 private function vtables_run_sql($sql, $table_name) { 196 global $wpdb; 197 if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table_name))) !== $table_name) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery 198 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 74 199 dbDelta($sql); 75 200 } … … 79 204 * ADD NEW COLUMN 80 205 */ 81 function vtables_add_new_column($tableName, $tableColumn, $columnType) {82 if ($columnType === 'JSON') {83 $create_ddl = "ALTER TABLE $tableName ADD $tableColumn JSON;";84 } else {85 $create_ddl = "ALTER TABLE $tableName ADD $tableColumn TEXT NULL DEFAULT NULL;";86 }87 88 require_once(ABSPATH . 'wp-admin/includes/upgrade.php');89 maybe_add_column($tableName, $tableColumn, $create_ddl);206 private function vtables_add_new_column($table_name, $column, $column_type) { 207 if ($column_type === 'JSON') { 208 $create_ddl = "ALTER TABLE $table_name ADD $column JSON;"; 209 } else { 210 $create_ddl = "ALTER TABLE $table_name ADD $column TEXT NULL DEFAULT NULL;"; 211 } 212 213 require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 214 maybe_add_column($table_name, $column, $create_ddl); 90 215 } 91 216 } -
vtables/trunk/includes/Classes/LoadAssets.php
r3576926 r3754379 5 5 if ( ! defined( 'ABSPATH' ) ) exit; 6 6 7 /** 8 * GET HASHED VUE FILES 9 */ 10 function vtables_get_hashed_file($name, $type) { 11 if ($name === 'index') { 12 $index_html = VTABLES_DIR . 'dist/spa/index.html'; 13 if (is_file($index_html)) { 14 $html = file_get_contents($index_html); 15 if ($html !== false && preg_match('#assets/(index\.[A-Za-z0-9]+\.' . preg_quote($type, '#') . ')#', $html, $m)) { 16 return $m[1]; 17 } 7 class LoadAssets { 8 9 /** 10 * DEV MODE 11 * env.php (never shipped, see release.sh) switches the admin screen and the 12 * embed document to the Quasar dev server: 13 * 14 * define( 'VTABLES_MODE', 'dev' ); 15 * define( 'VTABLES_BASE_URL', 'http://localhost:9000' ); 16 */ 17 public static function is_dev_mode() { 18 $env_file_path = VTABLES_DIR . 'env.php'; 19 20 if ( file_exists( $env_file_path ) ) { 21 include_once $env_file_path; 18 22 } 23 24 return defined( 'VTABLES_MODE' ) && constant( 'VTABLES_MODE' ) === 'dev' && defined( 'VTABLES_BASE_URL' ); 19 25 } 20 26 21 $folder = VTABLES_DIR . 'dist/spa/assets/'; 22 $files = list_files($folder); 23 $matchedFiles = []; 24 25 // Filter and store all matching files 26 foreach ( $files as $file ) { 27 if ( is_file( $file ) && strpos(basename($file), $name) !== false ) { 28 $filetype = wp_check_filetype($file); 29 if ($filetype['ext'] === $type) { 30 $matchedFiles[] = $file; 31 } 32 } 33 } 34 35 // If multiple files match, sort them to get a consistent result 36 if (count($matchedFiles) > 1) { 37 usort($matchedFiles, function($a, $b) { 38 return strcmp(basename($b), basename($a)); 39 }); 40 } 41 42 // Return the first matched file 43 return isset($matchedFiles[0]) ? basename($matchedFiles[0]) : null; 44 } 45 46 /** 47 * LOAD ASSETS CLASS 48 */ 49 class LoadAssets { 27 /** 28 * ADD TYPE ATTR 29 * The SPA entry is an ES module. 30 */ 50 31 public function add_type_attribute( $tag, $handle, $src ) { 51 if ( 'vtables-index' !== $handle) {52 return $tag;32 if ( $handle === 'vtables-script-boot' || $handle === 'vtables-index' ) { 33 $tag = str_replace( '<script ', '<script type="module" ', $tag ); 53 34 } 54 35 55 $tag = str_replace('<script ', '<script type="module" ', $tag);56 36 return $tag; 57 37 } 58 38 59 public function vtables_enqueue_assets() {60 $env_file_path = VTABLES_DIR . 'env.php';61 39 62 if (file_exists($env_file_path)) { 63 include($env_file_path); 64 $vtables_mode = constant('VTABLES_MODE'); 40 /** 41 * GET HASHED VUE FILES 42 * 43 * Vite emits <name>.<hash>.<ext> into dist/spa/assets, so the filename is only 44 * known at runtime. dist/spa/index.html is the authority on which of them is 45 * the entry: Vite writes the real filenames into it and 46 * scripts/patch-asset-base.mjs rewrites them to ./assets/<name>.<hash>.<ext>. 47 * 48 * Do NOT pick a name out of the directory listing instead. The pattern 49 * index.*.js is ambiguous even on a clean build — the entry chunk and a lazy 50 * index chunk both match — and an update unpacked over an older install (FTP 51 * or a file manager overwrites files but never deletes the ones that went 52 * away) leaves both builds' hashes side by side. 53 * 54 * glob() stays as the fallback for a dist/spa without index.html, ordered by 55 * mtime and then size so the newest build's entry chunk still wins. 56 * 57 * Results are memoized per request. 58 */ 59 public function vtables_get_hashed_file( $name, $type ) { 60 static $cache = array(); 61 62 $key = $name . '.' . $type; 63 if ( array_key_exists( $key, $cache ) ) { 64 return $cache[ $key ]; 65 65 } 66 66 67 if (isset($vtables_mode) && $vtables_mode === 'dev') { // dev mode67 $file = $this->vtables_hashed_file_from_manifest( $name, $type ); 68 68 69 wp_enqueue_style('vtables-material-icons', VTABLES_URL . 'public/css/material-icons-font-dist.css', false, VTABLES_VERSION); 70 wp_enqueue_script('vtables-script-boot', constant('VTABLES_BASE_URL') . '/.quasar/client-entry.js', array('jquery'), null, true); 69 if ( $file === null ) { 70 $file = $this->vtables_hashed_file_from_glob( $name, $type ); 71 } 71 72 72 } else { // prod mode73 $cache[ $key ] = $file; 73 74 74 wp_enqueue_style( 'vtables-material-icons', VTABLES_URL . 'dist/spa/css/material-icons-font-dist.css', false, VTABLES_VERSION); 75 return $file; 76 } 75 77 76 $app_css = VTABLES_URL . 'dist/spa/assets/' . vtables_get_hashed_file('index', 'css');77 wp_enqueue_style( 'vtables-index', $app_css, false, VTABLES_VERSION);78 78 79 wp_localize_script( 80 'wp-api', 81 'WP_API_Settings', 82 array( 83 'nonce' => wp_create_nonce('wp_rest'), 84 'base_url' => rest_url(), 85 ) 86 ); 87 wp_enqueue_script('wp-api'); 79 /** 80 * HASHED FILE FROM THE BUILD MANIFEST 81 * dist/spa/index.html as written by Vite — the only place that says which 82 * hash belongs to the current build. 83 */ 84 private function vtables_hashed_file_from_manifest( $name, $type ) { 85 $manifest = VTABLES_DIR . 'dist/spa/index.html'; 88 86 89 $app_js = VTABLES_URL . 'dist/spa/assets/' . vtables_get_hashed_file('index', 'js'); 90 wp_enqueue_script( 'vtables-index', $app_js, array('jquery'), VTABLES_VERSION, false); 87 if ( ! is_readable( $manifest ) ) { 88 return null; 89 } 91 90 92 add_filter( 'script_loader_tag', array( $this, 'add_type_attribute' ), 10, 3 );91 $html = file_get_contents( $manifest ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local build file 93 92 93 if ( ! is_string( $html ) || $html === '' ) { 94 return null; 94 95 } 96 97 // Case-sensitive on purpose: 'index' must not match 'IndexPage.<hash>.js'. 98 $pattern = '#assets/(' . preg_quote( $name, '#' ) . '\.[A-Za-z0-9_-]+\.' . preg_quote( $type, '#' ) . ')#'; 99 100 return preg_match( $pattern, $html, $matches ) ? $matches[1] : null; 101 } 102 103 104 /** 105 * HASHED FILE FROM THE DIRECTORY LISTING 106 * Fallback only. Newest build first, then the largest file: chunks from one 107 * build share an mtime (an unpacked zip gives them all the same one) and the 108 * entry is by far the biggest of them. 109 */ 110 private function vtables_hashed_file_from_glob( $name, $type ) { 111 $matched = glob( VTABLES_DIR . 'dist/spa/assets/' . $name . '.*.' . $type ); 112 113 if ( ! is_array( $matched ) || empty( $matched ) ) { 114 return null; 115 } 116 117 if ( count( $matched ) > 1 ) { 118 usort( $matched, function ( $a, $b ) { 119 $by_mtime = filemtime( $b ) <=> filemtime( $a ); 120 if ( $by_mtime !== 0 ) { 121 return $by_mtime; 122 } 123 124 $by_size = filesize( $b ) <=> filesize( $a ); 125 126 return $by_size !== 0 ? $by_size : strcmp( basename( $b ), basename( $a ) ); 127 } ); 128 } 129 130 return basename( $matched[0] ); 131 } 132 133 134 /** 135 * ENQUEUE ASSETS: for admin page 136 */ 137 public function vtables_enqueue_assets_admin() { 138 $sys_info = $this->vtables_get_sys_info( 'admin' ); 139 140 if ( self::is_dev_mode() ) { 141 wp_enqueue_style( 'vtables-material-icons', VTABLES_URL . 'public/css/material-icons-font-dist.css', array(), VTABLES_VERSION ); 142 wp_enqueue_style( 'vtables-css', VTABLES_URL . 'public/css/vtables.css', array(), VTABLES_VERSION ); 143 144 wp_enqueue_script( 'vtables-script-boot', constant( 'VTABLES_BASE_URL' ) . '/.quasar/client-entry.js', array(), VTABLES_VERSION, true ); 145 wp_localize_script( 'vtables-script-boot', 'vtables_sys_info', $sys_info ); 146 } else { 147 wp_enqueue_style( 'vtables-material-icons', VTABLES_URL . 'dist/spa/css/material-icons-font-dist.css', array(), VTABLES_VERSION ); 148 wp_enqueue_style( 'vtables-css', VTABLES_URL . 'public/css/vtables.css', array(), VTABLES_VERSION ); 149 150 // A missing hash would enqueue the assets directory itself; skip instead, 151 // so a broken dist fails as a missing script rather than a 403/404 on a 152 // directory URL. 153 $index_css = $this->vtables_get_hashed_file( 'index', 'css' ); 154 if ( $index_css ) { 155 wp_enqueue_style( 'vtables-index', VTABLES_URL . 'dist/spa/assets/' . $index_css, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- hashed build filename 156 } 157 158 $index_js = $this->vtables_get_hashed_file( 'index', 'js' ); 159 if ( $index_js ) { 160 wp_enqueue_script( 'vtables-index', VTABLES_URL . 'dist/spa/assets/' . $index_js, array(), null, false ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- hashed build filename 161 wp_localize_script( 'vtables-index', 'vtables_sys_info', $sys_info ); 162 } 163 } 164 165 add_filter( 'script_loader_tag', array( $this, 'add_type_attribute' ), 10, 3 ); 166 } 167 168 169 /** 170 * GET SYS INFO 171 * 'front' is printed into the embed document (the public SPA reads its own 172 * window first, see src/boot/wpapi.ts); 'admin' adds the nonce and upload 173 * limit for the admin screen. 174 */ 175 public function vtables_get_sys_info( $page_type = 'front' ) { 176 $arr_params = array( 177 'ajaxurl' => admin_url( 'admin-ajax.php' ), 178 'plain_links' => empty( get_option( 'permalink_structure' ) ), 179 'locale' => ( is_admin() && function_exists( 'get_user_locale' ) ) ? get_user_locale() : get_locale(), 180 'pro_url' => 'https://vtables.pro', 181 ); 182 183 if ( $page_type === 'admin' ) { 184 $arr_params['nonce'] = wp_create_nonce( 'vtables_nonce' ); 185 $arr_params['max_upload_size'] = wp_max_upload_size(); 186 } 187 188 return apply_filters( 'vtables_sys_info', $arr_params, $page_type ); 95 189 } 96 190 } 97 -
vtables/trunk/includes/autoload.php
r3036822 r3754379 13 13 * Plugin autoloader. 14 14 * 15 * Pattern (with or without <Subnamespace>):16 * <Namespace>/<Subnamespace>.../Class_Name (or Classname)17 * 'includes/subdir.../class-name.php' or '...classname.php'15 * Maps vTables\<Subnamespace>\<Class> to includes/<Subnamespace>/<Class>.php. 16 * The namespace separator is part of the match, so vTables Pro's 17 * vTablesPro\ classes are never resolved here. 18 18 * 19 * @since 3.0.0 20 * 21 * @param $class 19 * @param string $class 22 20 */ 23 21 function vtables_autoload($class) 24 22 { 25 // Do not load unless in plugin domain. 26 $namespace = 'vTables'; 23 $namespace = 'vTables\\'; 27 24 if (strpos($class, $namespace) !== 0) { 28 25 return; 29 26 } 30 27 31 // Remove the root namespace.32 28 $unprefixed = substr($class, strlen($namespace)); 33 34 // Build the file path. 35 $file_path = str_replace('\\', DIRECTORY_SEPARATOR, $unprefixed); 36 37 $file = dirname(__FILE__) . $file_path . '.php'; 29 $file_path = str_replace('\\', DIRECTORY_SEPARATOR, $unprefixed); 30 $file = __DIR__ . DIRECTORY_SEPARATOR . $file_path . '.php'; 38 31 39 32 if (file_exists($file)) { … … 41 34 } 42 35 } 43 // Register the autoloader. 36 44 37 spl_autoload_register('vtables_autoload'); 45 38 } -
vtables/trunk/includes/endpoints.php
r3576926 r3754379 3 3 4 4 /** 5 * GET ALL TABLES. 6 */ 7 add_action( 'rest_api_init', function () { 8 register_rest_route( 'vtables/v1', '/tables', array( 9 'methods' => 'GET', 10 'callback' => 'vtables_get_all_handler', 11 'args' => [ 12 'page' => [ 13 'description' => 'Current page', 14 'type' => "integer", 15 ], 16 'per_page' => [ 17 'description' => 'Items per page', 18 'type' => "integer", 19 ], 20 'search' => [ 21 'description' => 'Search items', 22 'type' => "string", 23 ] 24 ], 25 'permission_callback' => '__return_true', 26 ) ); 27 } ); 28 29 function vtables_get_all_handler( WP_REST_Request $data ) { 5 * LIST TABLES. 6 */ 7 add_action('wp_ajax_vtables_list_tables', 'vtables_list_tables_handler'); 8 function vtables_list_tables_handler() { 9 vtables_require_admin_ajax(); 10 30 11 global $wpdb; 31 32 $table_name = $wpdb->prefix . 'vtables'; 33 $limit = $data['per_page']; 34 $offset = ($data['page'] - 1) * $data['per_page']; 35 $search = $data['search'] ?? ''; 12 $allowed_orderby = array('created_at', 'updated_at', 'title', 'table_type'); 13 $limit = isset($_GET['per_page']) ? max(1, min(100, intval($_GET['per_page']))) : 10; // phpcs:ignore WordPress.Security.NonceVerification 14 $page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1; // phpcs:ignore WordPress.Security.NonceVerification 15 $offset = ($page - 1) * $limit; 16 $search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 17 $orderby = isset($_GET['orderby']) && in_array(sanitize_text_field(wp_unslash($_GET['orderby'])), $allowed_orderby, true) // phpcs:ignore WordPress.Security.NonceVerification 18 ? sanitize_text_field(wp_unslash($_GET['orderby'])) // phpcs:ignore WordPress.Security.NonceVerification 19 : 'created_at'; 20 $order = isset($_GET['order']) && in_array(strtoupper(sanitize_text_field(wp_unslash($_GET['order']))), array('ASC', 'DESC'), true) // phpcs:ignore WordPress.Security.NonceVerification 21 ? strtoupper(sanitize_text_field(wp_unslash($_GET['order']))) // phpcs:ignore WordPress.Security.NonceVerification 22 : 'DESC'; 23 36 24 $keyword = $wpdb->esc_like($search); 37 $orderby = isset($data['orderby']) ? $data['orderby'] : 'created_at'; 38 $order = isset($data['order']) && in_array($data['order'], ['ASC', 'DESC']) ? $data['order'] : 'DESC'; 39 40 $search_by_title_query = $wpdb->prepare( 41 "SELECT * FROM $table_name WHERE title LIKE %s", 42 '%' . $keyword . '%' 25 26 $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 27 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vtables WHERE title LIKE %s", '%' . $keyword . '%' ) 43 28 ); 44 29 45 $pagination_query = $wpdb->prepare( 46 "SELECT * FROM $table_name WHERE title LIKE %s ORDER BY $orderby $order LIMIT $limit OFFSET $offset", 47 '%' . $keyword . '%' 30 // $orderby and $order are allowlisted above; sanitize_sql_orderby() keeps 31 // the clause SQL-safe on its own. 32 $order_sql = sanitize_sql_orderby("$orderby $order"); 33 $results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 34 $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}vtables WHERE title LIKE %s ORDER BY $order_sql LIMIT %d OFFSET %d", '%' . $keyword . '%', $limit, $offset ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 48 35 ); 49 36 50 $total = $wpdb->get_results($search_by_title_query); 51 $results = $wpdb->get_results($pagination_query); 52 53 if (!$results) { 54 new WP_Error( 'TABLES_NOT_FOUND', 'Tables Not found.', array( 'status' => 404 ) ); 37 if ($wpdb->last_error) { 38 vtables_send_db_error(); 55 39 } 56 40 … … 58 42 $item->settings = json_decode($item->settings); 59 43 $item->source = json_decode($item->source); 60 } 61 62 $response = array(); 63 $response['tables'] = $results; 64 $response['total'] = count($total); 65 66 $rest_response = new WP_REST_Response( 44 // Pro-only tables (see vtables_table_requires_pro): the list shows them 45 // (their data is untouched) but they can't be opened here. 46 $item->requires_pro = vtables_table_requires_pro($item); 47 unset($item->woocommerce_categories, $item->plan); 48 if ($item->table_type === 'importing') { 49 $transient = get_transient('vtables_import_' . $item->id); 50 if ($transient) { 51 $item->import_progress = array( 52 'imported_rows' => intval($transient['imported_rows']), 53 'total_rows' => intval($transient['total_rows']), 54 ); 55 } 56 } 57 } 58 59 $response = array(); 60 $response['tables'] = $results; 61 $response['total'] = $total; 62 63 wp_send_json($response); 64 } 65 66 67 /** 68 * DB ERROR RESPONSE 69 * The raw $wpdb->last_error describes the schema and query; it goes to the 70 * debug log (when enabled), never to the client. 71 */ 72 function vtables_send_db_error() { 73 global $wpdb; 74 75 if (defined('WP_DEBUG') && WP_DEBUG && $wpdb->last_error) { 76 error_log('vTables DB error: ' . $wpdb->last_error); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log 77 } 78 79 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Database error.'), 500); 80 } 81 82 83 /** 84 * STANDARD GUARDS 85 * Every admin action: valid vtables_nonce on an AJAX request, manage_options. 86 */ 87 function vtables_require_admin_ajax() { 88 if ( 89 !check_ajax_referer('vtables_nonce', 'security', false) || 90 !defined('DOING_AJAX') || 91 !DOING_AJAX 92 ) { 93 wp_send_json(array('code' => 400, 'error' => 'INVALID_REQUEST', 'message' => 'Invalid request.'), 400); 94 } 95 96 if (!current_user_can('manage_options')) { 97 wp_send_json(array('code' => 403, 'error' => 'PERMISSION_DENIED', 'message' => 'Permission denied.'), 403); 98 } 99 } 100 101 102 /** 103 * REJECT PRO-ONLY TABLES (AJAX GUARD) 104 * Google Sheets / external / WooCommerce tables, and tables with a saved row 105 * filter (see vtables_table_requires_pro), are left untouched for vTables 106 * Pro — none of the read/edit endpoints serve them here. 107 */ 108 function vtables_reject_pro_table($table_id) { 109 if (vtables_table_requires_pro(vtables_table_meta($table_id))) { 110 wp_send_json(array('code' => 403, 'error' => 'REQUIRES_PRO', 'message' => 'This table requires vTables Pro.'), 403); 111 } 112 } 113 114 115 /** 116 * FETCH TABLE BY ID. 117 */ 118 add_action('wp_ajax_vtables_fetch_table', 'vtables_fetch_table_handler'); 119 add_action('wp_ajax_nopriv_vtables_fetch_table', 'vtables_fetch_table_handler'); 120 function vtables_fetch_table_handler() { 121 if (!defined('DOING_AJAX') || !DOING_AJAX) { 122 wp_send_json(array('code' => 400, 'error' => 'INVALID_REQUEST', 'message' => 'Invalid request.'), 400); 123 } 124 125 if (empty($_GET['id'])) { // phpcs:ignore WordPress.Security.NonceVerification 126 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing required fields.'), 400); 127 } 128 129 global $wpdb; 130 $id = intval($_GET['id']); // phpcs:ignore WordPress.Security.NonceVerification 131 132 $table = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 133 $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}vtables WHERE id = %d", $id ) 134 ); 135 136 if (!$table) { 137 wp_send_json(array('code' => 404, 'error' => 'TABLE_NOT_FOUND', 'message' => 'Table by ID not found.'), 404); 138 } 139 140 // Pro-only sources / saved row filter: metadata only. 141 if (vtables_table_requires_pro($table)) { 142 wp_send_json(array( 143 'id' => (int) $table->id, 144 'title' => $table->title, 145 'table_type' => $table->table_type, 146 'requires_pro' => true, 147 'settings' => null, 148 'source' => array('columns' => array(), 'rows' => array()), 149 )); 150 } 151 152 $table->settings = json_decode($table->settings); 153 $table->source = json_decode($table->source); 154 $table->requires_pro = false; 155 unset($table->woocommerce_categories, $table->plan); 156 157 // Rows live in wp_vtables_source (one record per data row). Legacy tables 158 // (rows still inside the source cell, not yet moved by the background 159 // migration) have no records here, so their source->rows stay as a fallback. 160 $row_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 161 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $id ) 162 ); 163 164 if ($row_count > 0) { 165 if (!is_object($table->source)) { 166 $table->source = new stdClass(); 167 } 168 169 // ALWAYS SERVER MODE for tables whose rows live in wp_vtables_source: the 170 // SPA pages through vtables_query_rows instead of loading every row. 171 $per_page = vtables_default_per_page(); 172 173 $first_page = vtables_query_source_rows(array( 174 'table_id' => $id, 175 'page' => 1, 176 'per_page' => $per_page, 177 )); 178 179 $table->server_mode = true; 180 $table->total_rows = $row_count; 181 $table->source->rows = array(); 182 $table->first_page = $first_page['rows']; 183 $table->per_page = $per_page; 184 } 185 186 wp_send_json($table); 187 } 188 189 190 /** 191 * Find a title that doesn't collide with an existing row, appending 192 * -copy, -copy-2, -copy-3, ... until unique. A single -copy check only 193 * survives one duplication; duplicating the same title again would 194 * collide with the previous "-copy" row, so this keeps incrementing. 195 */ 196 function vtables_generate_unique_title($wpdb, $table_name, $title) { 197 if (!$title) { 198 return $title; 199 } 200 201 $candidate = $title; 202 $suffix = 1; 203 204 while ( 205 $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 206 $wpdb->prepare("SELECT id FROM $table_name WHERE title = %s", $candidate) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared 207 ) 208 ) { 209 $candidate = $suffix === 1 ? $title . '-copy' : $title . '-copy-' . $suffix; 210 $suffix++; 211 } 212 213 return $candidate; 214 } 215 216 217 /** 218 * Seed settings.columns.visibleColumns from the parsed column list when the 219 * incoming settings carry no explicit show/hide selection (both lists empty 220 * or missing). The renderer treats an empty visibleColumns as "show all", 221 * but the Settings → Columns toggles bind to it directly, so a table stored 222 * with an empty list shows every toggle off and one click hides everything. 223 * Legacy uploaders seed this client-side; the chunked/stream import paths 224 * send INITIAL_SETTINGS verbatim, so it has to happen here. 225 */ 226 function vtables_seed_visible_columns($settings_json, $columns) { 227 $decoded = json_decode((string) $settings_json, true); 228 if (!is_array($decoded)) { 229 return $settings_json; 230 } 231 232 $cols = isset($decoded['columns']) && is_array($decoded['columns']) ? $decoded['columns'] : array(); 233 $visible = isset($cols['visibleColumns']) && is_array($cols['visibleColumns']) ? $cols['visibleColumns'] : array(); 234 $hidden = isset($cols['hiddenColumns']) && is_array($cols['hiddenColumns']) ? $cols['hiddenColumns'] : array(); 235 236 if (!empty($visible) || !empty($hidden)) { 237 return $settings_json; 238 } 239 240 $names = array(); 241 foreach ((array) $columns as $col) { 242 if (is_array($col) && isset($col['name']) && $col['name'] !== '') { 243 $names[] = (string) $col['name']; 244 } 245 } 246 247 if (empty($names)) { 248 return $settings_json; 249 } 250 251 $cols['visibleColumns'] = $names; 252 $cols['hiddenColumns'] = array(); 253 $decoded['columns'] = $cols; 254 255 $encoded = wp_json_encode($decoded); 256 return $encoded !== false ? $encoded : $settings_json; 257 } 258 259 260 /** 261 * CREATE TABLE. 262 */ 263 add_action('wp_ajax_vtables_create_table', 'vtables_create_table_handler'); 264 function vtables_create_table_handler() { 265 vtables_require_admin_ajax(); 266 267 global $wpdb; 268 $table_name = $wpdb->prefix . 'vtables'; 269 270 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 271 $source = isset($_POST['source']) ? wp_unslash($_POST['source']) : '{}'; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, decoded below 272 $file_size = isset($_POST['file_size']) ? sanitize_text_field(wp_unslash($_POST['file_size'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 273 $settings = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '{}'; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, filtered below 274 $table_type = isset($_POST['table_type']) ? sanitize_key(wp_unslash($_POST['table_type'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 275 276 // Only file/manual sources are created here; the other sources are vTables Pro's. 277 if (!in_array($table_type, vtables_free_table_types(), true)) { 278 wp_send_json(array('code' => 403, 'error' => 'REQUIRES_PRO', 'message' => 'This data source requires vTables Pro.'), 403); 279 } 280 281 // Settings go through the same Free settings policy as every save, so a 282 // crafted payload can't seed Pro-only settings onto a new table. 283 $settings = vtables_filter_settings_for_free($settings, '{}'); 284 285 $title = vtables_generate_unique_title($wpdb, $table_name, $title); 286 287 // Split source: keep {columns, rows:[]} in wp_vtables; store the rows in 288 // wp_vtables_source via the shared helper. Keeps a single read path in fetch_table 289 // and keeps large datasets out of the one longtext cell. 290 $source_rows = array(); 291 $source_decoded = json_decode($source, true); 292 if (!is_array($source_decoded)) { 293 wp_send_json(array('code' => 400, 'error' => 'INVALID_SOURCE', 'message' => 'Invalid table source.'), 400); 294 } 295 if (isset($source_decoded['rows']) && is_array($source_decoded['rows'])) { 296 $source_rows = $source_decoded['rows']; 297 } 298 $source_decoded['rows'] = array(); 299 $source_to_store = wp_json_encode($source_decoded); 300 if ($source_to_store !== false) { 301 $source = $source_to_store; 302 } 303 304 $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 305 $table_name, 67 306 array( 68 'success' => !!$results, 69 'data' => $response, 307 'title' => $title, 308 'source' => $source, 309 'file_size' => $file_size, 310 'settings' => $settings, 311 'table_type' => $table_type, 312 // Stamped like vTables Pro stamps tables created without a license, so 313 // Pro later treats it as the Free-plan table it is. 314 'plan' => 'free', 70 315 ) 71 316 ); 72 317 73 vtables_maybe_send_nocache_headers($rest_response); 74 75 return $rest_response; 76 } 77 78 79 /** 80 * GET TABLE BY ID. 81 */ 82 add_action( 'rest_api_init', function () { 83 register_rest_route( 'vtables/v1', '/tables/(?P<id>\d+)', array( 84 'methods' => 'GET', 85 'callback' => 'vtables_get_by_id_handler', 86 'permission_callback' => '__return_true', 87 ) ); 88 } ); 89 90 function vtables_get_by_id_handler( WP_REST_Request $data ) { 318 if ($result === false) { 319 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to create record.'), 500); 320 } 321 322 $new_table_id = $wpdb->insert_id; 323 324 if (!empty($source_rows)) { 325 vtables_replace_source_rows($new_table_id, $source_rows); 326 } 327 328 wp_send_json(array('id' => $new_table_id)); 329 } 330 331 332 /** 333 * SAVE TABLE CHANGES. 334 */ 335 add_action('wp_ajax_vtables_save_source', 'vtables_save_source_handler'); 336 function vtables_save_source_handler() { 337 vtables_require_admin_ajax(); 338 91 339 global $wpdb; 92 340 $table_name = $wpdb->prefix . 'vtables'; 93 $id = $data['id']; 94 95 $query = $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id); 96 $results = $wpdb->get_results($query); 97 98 if (empty($results)) { 99 return new WP_Error( 'TABLE_NOT_FOUND', 'Table by ID not found.', array( 'status' => 404 ) ); 100 } 101 102 $results[0]->settings = json_decode($results[0]->settings); 103 $results[0]->source = json_decode($results[0]->source); 104 $response = $results[0]; 105 106 $rest_response = new WP_REST_Response( 341 342 $id = isset($_POST['id']) ? intval($_POST['id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 343 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 344 $source = isset($_POST['source']) ? wp_unslash($_POST['source']) : ''; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, decoded below 345 $file_size = isset($_POST['fileSize']) ? sanitize_text_field(wp_unslash($_POST['fileSize'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 346 347 if (!$id) { 348 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing required fields.'), 400); 349 } 350 351 if (!vtables_table_exists($id)) { 352 wp_send_json(array('code' => 404, 'error' => 'TABLE_NOT_FOUND', 'message' => 'Table not found.'), 404); 353 } 354 355 vtables_reject_pro_table($id); 356 357 // Update only title 358 if ($title && empty($source)) { 359 $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 360 $table_name, 361 array( 362 'title' => $title, 363 'updated_at' => current_time('mysql'), 364 ), 365 array('id' => $id) 366 ); 367 wp_send_json($result); 368 } 369 370 // Split source: persist {columns, rows:[]} on wp_vtables, store rows in 371 // wp_vtables_source. (Still bounded by post_max_size on the incoming request — 372 // pre-existing; fully addressed by the per-row edit endpoints in Phase 2.) 373 $source_rows = array(); 374 $source_decoded = json_decode($source, true); 375 if (is_array($source_decoded)) { 376 if (isset($source_decoded['rows']) && is_array($source_decoded['rows'])) { 377 $source_rows = $source_decoded['rows']; 378 } 379 $source_decoded['rows'] = array(); 380 $source_to_store = wp_json_encode($source_decoded); 381 if ($source_to_store !== false) { 382 $source = $source_to_store; 383 } 384 } 385 386 // Update source 387 $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 388 $table_name, 107 389 array( 108 'success' => !!$results, 109 'data' => $response, 390 'title' => $title, 391 'file_size' => $file_size, 392 'source' => $source, 393 'updated_at' => current_time('mysql'), 394 ), 395 array('id' => $id), 396 array('%s', '%s', '%s', '%s'), 397 array('%d') 398 ); 399 400 // Only replace stored rows when the payload actually carries rows. In server 401 // mode the client holds only one page, so source.rows is empty here — replacing 402 // would destroy the dataset. Per-row edits go through the dedicated endpoints. 403 if (!empty($source_rows)) { 404 vtables_replace_source_rows($id, $source_rows); 405 } 406 407 $updated_table = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 408 $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}vtables WHERE id = %d", $id ) 409 ); 410 $updated_table->source = json_decode($updated_table->source); 411 412 // Reattach saved rows so the response mirrors the submitted source. 413 if (!empty($source_rows)) { 414 if (!is_object($updated_table->source)) { 415 $updated_table->source = new stdClass(); 416 } 417 $updated_table->source->rows = json_decode(wp_json_encode($source_rows)); 418 } 419 420 wp_send_json($updated_table); 421 } 422 423 424 /** 425 * SAVE TABLE SETTINGS. 426 */ 427 add_action('wp_ajax_vtables_save_settings', 'vtables_save_settings_handler'); 428 function vtables_save_settings_handler() { 429 vtables_require_admin_ajax(); 430 431 global $wpdb; 432 $table_name = $wpdb->prefix . 'vtables'; 433 434 $id = isset($_POST['id']) ? intval($_POST['id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 435 $settings = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '{}'; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, filtered below 436 $updated_at = current_time('mysql'); 437 438 if (!$id) { 439 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing required fields.'), 400); 440 } 441 442 $table_row = $wpdb->get_row( $wpdb->prepare( "SELECT id, table_type, settings FROM {$wpdb->prefix}vtables WHERE id = %d", $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 443 444 if ($table_row === null) { 445 wp_send_json(array('code' => 404, 'error' => 'TABLE_NOT_FOUND', 'message' => 'Table not found.'), 404); 446 } 447 448 vtables_reject_pro_table($id); 449 450 // Settings that belong to vTables Pro keep their stored values: this plugin 451 // neither adds nor erases them (see includes/settings-policy.php), so a 452 // table's Pro settings survive editing here and apply again under Pro. 453 $settings = vtables_filter_settings_for_free($settings, (string) $table_row->settings); 454 455 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 456 $table_name, 457 array( 458 'settings' => $settings, 459 'updated_at' => $updated_at, 460 ), 461 array('id' => $id), 462 array('%s', '%s'), 463 array('%d') 464 ); 465 466 wp_send_json(array('updated_at' => $updated_at)); 467 } 468 469 470 /** 471 * DELETE TABLE. 472 */ 473 add_action('wp_ajax_vtables_delete_table', 'vtables_delete_table_handler'); 474 function vtables_delete_table_handler() { 475 vtables_require_admin_ajax(); 476 477 if (empty($_POST['id'])) { // phpcs:ignore WordPress.Security.NonceVerification 478 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table ID.'), 400); 479 } 480 481 global $wpdb; 482 $table_name = $wpdb->prefix . 'vtables'; 483 $id = intval($_POST['id']); // phpcs:ignore WordPress.Security.NonceVerification 484 485 // $wpdb->delete() returns 0 (not false) for a row that was never there, so 486 // without this the caller gets an indistinguishable 200 for a bogus id. 487 if (!vtables_table_exists($id)) { 488 wp_send_json(array('code' => 404, 'error' => 'TABLE_NOT_FOUND', 'message' => 'Table not found.'), 404); 489 } 490 491 $result = $wpdb->delete( $table_name, array('id' => $id) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 492 493 if ($result === false) { 494 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to delete table.'), 500); 495 } 496 497 // Remove the table's rows so we don't orphan records in wp_vtables_source. 498 $wpdb->delete( $wpdb->prefix . 'vtables_source', array('table_id' => $id), array('%d') ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 499 500 wp_send_json(array('id' => $id)); 501 } 502 503 504 /** 505 * PARSE EXCEL FILE. 506 */ 507 add_action('wp_ajax_vtables_parse_excel', 'vtables_parse_excel_handler'); 508 function vtables_parse_excel_handler() { 509 vtables_require_admin_ajax(); 510 511 if (!isset($_FILES['file']['error'], $_FILES['file']['name'], $_FILES['file']['size'], $_FILES['file']['tmp_name']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { // phpcs:ignore WordPress.Security.NonceVerification 512 wp_send_json(array('code' => 400, 'error' => 'MISSING_FILE', 'message' => 'No file uploaded or upload error.'), 400); 513 } 514 515 $allowed_extensions = array('xlsx', 'xls'); 516 $file_name = sanitize_file_name(wp_unslash($_FILES['file']['name'])); // phpcs:ignore WordPress.Security.NonceVerification 517 $extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); 518 519 if (!in_array($extension, $allowed_extensions, true)) { 520 wp_send_json(array('code' => 400, 'error' => 'INVALID_FILE_TYPE', 'message' => 'Only .xlsx and .xls files are allowed.'), 400); 521 } 522 523 $max_size = 52428800; // 50 MB 524 if (intval($_FILES['file']['size']) > $max_size) { // phpcs:ignore WordPress.Security.NonceVerification 525 wp_send_json(array('code' => 400, 'error' => 'FILE_TOO_LARGE', 'message' => 'File exceeds the 50 MB size limit.'), 400); 526 } 527 528 $temp_dir = vtables_uploads_dir(); 529 530 if (!$temp_dir) { 531 wp_send_json(array('code' => 500, 'error' => 'UPLOAD_DIR_ERROR', 'message' => 'Could not create upload directory.'), 500); 532 } 533 534 $temp_file = vtables_store_uploaded_file($_FILES['file'], $temp_dir, wp_unique_filename($temp_dir, $file_name)); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by wp_handle_upload() 535 536 if (!$temp_file) { 537 wp_send_json(array('code' => 500, 'error' => 'FILE_MOVE_ERROR', 'message' => 'Could not move uploaded file.'), 500); 538 } 539 540 $raw_rows = array(); 541 542 if ($extension === 'xlsx') { 543 $xlsx = \Shuchkin\SimpleXLSX::parse($temp_file); 544 if (!$xlsx) { 545 wp_delete_file($temp_file); 546 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xlsx file.'), 500); 547 } 548 $raw_rows = $xlsx->rows(); 549 } else { 550 $xls = \Shuchkin\SimpleXLS::parse($temp_file); 551 if (!$xls) { 552 wp_delete_file($temp_file); 553 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xls file.'), 500); 554 } 555 $raw_rows = $xls->rows(); 556 } 557 558 wp_delete_file($temp_file); 559 560 if (empty($raw_rows)) { 561 wp_send_json(array('code' => 400, 'error' => 'EMPTY_FILE', 'message' => 'The file contains no data.'), 400); 562 } 563 564 $headers = array_shift($raw_rows); 565 $columns = array(); 566 foreach ($headers as $header) { 567 $name = (string) $header; 568 $columns[] = array('name' => $name, 'label' => $name); 569 } 570 571 $rows = array(); 572 foreach ($raw_rows as $raw_row) { 573 // Skip entirely empty rows. 574 $non_empty = array_filter($raw_row, function($v) { return $v !== '' && $v !== null; }); 575 if (empty($non_empty)) { 576 continue; 577 } 578 $row_data = array(); 579 foreach ($headers as $index => $header) { 580 $row_data[(string) $header] = isset($raw_row[$index]) ? $raw_row[$index] : ''; 581 } 582 $rows[] = $row_data; 583 } 584 585 wp_send_json(array('columns' => $columns, 'rows' => $rows)); 586 } 587 588 589 /** 590 * UPLOAD CHUNK. 591 * Accepts one binary chunk of a large file and stores it to disk. 592 */ 593 add_action('wp_ajax_vtables_upload_chunk', 'vtables_upload_chunk_handler'); 594 function vtables_upload_chunk_handler() { 595 vtables_require_admin_ajax(); 596 597 // Validate and sanitize inputs. 598 $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 599 $chunk_index = isset($_POST['chunk_index']) ? intval($_POST['chunk_index']) : -1; // phpcs:ignore WordPress.Security.NonceVerification 600 $total_chunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 601 602 if (!preg_match('/^[0-9a-f\-]{36}$/i', $session_id) || $chunk_index < 0 || $total_chunks < 1) { 603 wp_send_json(array('code' => 400, 'error' => 'INVALID_PARAMS', 'message' => 'Invalid parameters.'), 400); 604 } 605 606 if (!isset($_FILES['file']['error'], $_FILES['file']['name'], $_FILES['file']['size'], $_FILES['file']['tmp_name']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { // phpcs:ignore WordPress.Security.NonceVerification 607 wp_send_json(array('code' => 400, 'error' => 'MISSING_FILE', 'message' => 'No file chunk uploaded or upload error.'), 400); 608 } 609 610 // Chunk must not exceed the server upload limit on its own. 611 if (intval($_FILES['file']['size']) > wp_max_upload_size()) { // phpcs:ignore WordPress.Security.NonceVerification 612 wp_send_json(array('code' => 400, 'error' => 'CHUNK_TOO_LARGE', 'message' => 'Chunk exceeds server upload limit.'), 400); 613 } 614 615 $uploads_dir = vtables_uploads_dir(); 616 $chunk_dir = $uploads_dir ? $uploads_dir . 'chunks/' . $session_id . '/' : ''; 617 618 if (!$uploads_dir || !wp_mkdir_p($chunk_dir)) { 619 wp_send_json(array('code' => 500, 'error' => 'UPLOAD_DIR_ERROR', 'message' => 'Could not create chunk directory.'), 500); 620 } 621 622 if (!vtables_store_uploaded_file($_FILES['file'], $chunk_dir, 'chunk_' . $chunk_index)) { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated by wp_handle_upload() 623 wp_send_json(array('code' => 500, 'error' => 'FILE_MOVE_ERROR', 'message' => 'Could not store chunk.'), 500); 624 } 625 626 wp_send_json(array('received' => $chunk_index)); 627 } 628 629 630 /** 631 * FINALIZE UPLOAD. 632 * Assembles chunks, extracts column headers, creates a table skeleton with 633 * table_type = 'importing', and persists import state in a WP transient. 634 */ 635 add_action('wp_ajax_vtables_finalize_upload', 'vtables_finalize_upload_handler'); 636 function vtables_finalize_upload_handler() { 637 vtables_require_admin_ajax(); 638 639 $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 640 $file_name = isset($_POST['file_name']) ? sanitize_file_name(wp_unslash($_POST['file_name'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 641 $title = isset($_POST['title']) ? sanitize_text_field(wp_unslash($_POST['title'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 642 $total_chunks = isset($_POST['total_chunks']) ? intval($_POST['total_chunks']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 643 $file_size = isset($_POST['file_size']) ? sanitize_text_field(wp_unslash($_POST['file_size'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 644 $settings = isset($_POST['settings']) ? wp_unslash($_POST['settings']) : '{}'; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, filtered below 645 $original_type_override = isset($_POST['original_type']) ? sanitize_key(wp_unslash($_POST['original_type'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 646 647 // The override may only name another file/manual type. 648 if ($original_type_override !== '' && !in_array($original_type_override, vtables_free_table_types(), true)) { 649 $original_type_override = ''; 650 } 651 652 if (!preg_match('/^[0-9a-f\-]{36}$/i', $session_id) || !$file_name || $total_chunks < 1) { 653 wp_send_json(array('code' => 400, 'error' => 'INVALID_PARAMS', 'message' => 'Invalid parameters.'), 400); 654 } 655 656 $allowed_extensions = array('xlsx', 'xls', 'csv', 'json'); 657 $extension = strtolower(pathinfo($file_name, PATHINFO_EXTENSION)); 658 659 if (!in_array($extension, $allowed_extensions, true)) { 660 wp_send_json(array('code' => 400, 'error' => 'INVALID_FILE_TYPE', 'message' => 'Unsupported file type.'), 400); 661 } 662 663 $assembled_dir = vtables_uploads_dir(); 664 $chunk_dir = $assembled_dir . 'chunks/' . $session_id . '/'; 665 666 if (!$assembled_dir) { 667 wp_send_json(array('code' => 500, 'error' => 'UPLOAD_DIR_ERROR', 'message' => 'Could not create upload directory.'), 500); 668 } 669 670 // Assemble chunks in order. 671 $assembled_path = $assembled_dir . $session_id . '.' . $extension; 672 $out = fopen($assembled_path, 'wb'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 673 if (!$out) { 674 wp_send_json(array('code' => 500, 'error' => 'ASSEMBLE_ERROR', 'message' => 'Could not create assembled file.'), 500); 675 } 676 677 for ($i = 0; $i < $total_chunks; $i++) { 678 $chunk_file = $chunk_dir . 'chunk_' . $i; 679 if (!file_exists($chunk_file)) { 680 fclose($out); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 681 wp_delete_file($assembled_path); 682 wp_send_json(array('code' => 400, 'error' => 'MISSING_CHUNK', 'message' => 'Missing chunk ' . $i . '.'), 400); 683 } 684 $chunk_data = file_get_contents($chunk_file); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 685 fwrite($out, $chunk_data); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 686 } 687 fclose($out); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 688 689 // Remove chunk directory. 690 vtables_remove_directory($chunk_dir); 691 692 // Hard 10 MB cap (defense in depth; the client rejects earlier). A larger 693 // assembled file is removed before any parser loads it into memory — a 10 MB 694 // xlsx can already expand to hundreds of MB inside SimpleXLSX on a 128 MB host. 695 if (filesize($assembled_path) > vtables_max_upload_bytes()) { 696 wp_delete_file($assembled_path); 697 wp_send_json(array('code' => 400, 'error' => 'FILE_TOO_LARGE', 'message' => 'File exceeds the 10 MB size limit.'), 400); 698 } 699 700 // Extract column headers, count rows, and collect a small sample for type 701 // sniffing (first ~200 data rows). $original_extension is kept before xlsx/xls 702 // (and now json) get normalized to a streamable CSV cache. 703 $columns = array(); 704 $total_rows = 0; 705 $original_extension = $extension; 706 $sample_rows = array(); 707 $sample_limit = 200; 708 709 if ($extension === 'xlsx') { 710 $xlsx = \Shuchkin\SimpleXLSX::parse($assembled_path); 711 if (!$xlsx) { 712 wp_delete_file($assembled_path); 713 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xlsx file.'), 500); 714 } 715 $raw_rows = $xlsx->rows(); 716 if (empty($raw_rows)) { 717 wp_delete_file($assembled_path); 718 wp_send_json(array('code' => 400, 'error' => 'EMPTY_FILE', 'message' => 'The file contains no data.'), 400); 719 } 720 $headers = array_shift($raw_rows); 721 foreach ($headers as $header) { 722 $name = (string) $header; 723 $columns[] = array('name' => $name, 'label' => $name); 724 } 725 // Convert to a streamable CSV cache so each batch call reads O(batch) not O(total). 726 $csv_cache_path = $assembled_dir . $session_id . '_cache.csv'; 727 $fh_out = fopen($csv_cache_path, 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 728 if ($fh_out) { 729 fputcsv($fh_out, $headers); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 730 $actual_rows = 0; 731 foreach ($raw_rows as $raw_row) { 732 $non_empty = array_filter($raw_row, function ($v) { return $v !== '' && $v !== null; }); 733 if (empty($non_empty)) continue; 734 $padded = array(); 735 $assoc = array(); 736 foreach ($headers as $idx => $h) { 737 $cell = isset($raw_row[$idx]) ? $raw_row[$idx] : ''; 738 $padded[] = $cell; 739 $assoc[(string) $h] = $cell; 740 } 741 fputcsv($fh_out, $padded); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 742 if (count($sample_rows) < $sample_limit) { 743 $sample_rows[] = $assoc; 744 } 745 $actual_rows++; 746 } 747 fclose($fh_out); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 748 wp_delete_file($assembled_path); 749 $assembled_path = $csv_cache_path; 750 $extension = 'csv'; 751 $total_rows = $actual_rows; 752 } else { 753 $total_rows = count($raw_rows); 754 } 755 } elseif ($extension === 'xls') { 756 $xls = \Shuchkin\SimpleXLS::parse($assembled_path); 757 if (!$xls) { 758 wp_delete_file($assembled_path); 759 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xls file.'), 500); 760 } 761 $raw_rows = $xls->rows(); 762 if (empty($raw_rows)) { 763 wp_delete_file($assembled_path); 764 wp_send_json(array('code' => 400, 'error' => 'EMPTY_FILE', 'message' => 'The file contains no data.'), 400); 765 } 766 $headers = array_shift($raw_rows); 767 foreach ($headers as $header) { 768 $name = (string) $header; 769 $columns[] = array('name' => $name, 'label' => $name); 770 } 771 // Convert to a streamable CSV cache so each batch call reads O(batch) not O(total). 772 $csv_cache_path = $assembled_dir . $session_id . '_cache.csv'; 773 $fh_out = fopen($csv_cache_path, 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 774 if ($fh_out) { 775 fputcsv($fh_out, $headers); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 776 $actual_rows = 0; 777 foreach ($raw_rows as $raw_row) { 778 $non_empty = array_filter($raw_row, function ($v) { return $v !== '' && $v !== null; }); 779 if (empty($non_empty)) continue; 780 $padded = array(); 781 $assoc = array(); 782 foreach ($headers as $idx => $h) { 783 $cell = isset($raw_row[$idx]) ? $raw_row[$idx] : ''; 784 $padded[] = $cell; 785 $assoc[(string) $h] = $cell; 786 } 787 fputcsv($fh_out, $padded); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 788 if (count($sample_rows) < $sample_limit) { 789 $sample_rows[] = $assoc; 790 } 791 $actual_rows++; 792 } 793 fclose($fh_out); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 794 wp_delete_file($assembled_path); 795 $assembled_path = $csv_cache_path; 796 $extension = 'csv'; 797 $total_rows = $actual_rows; 798 } else { 799 $total_rows = count($raw_rows); 800 } 801 } elseif ($extension === 'csv') { 802 $fh = fopen($assembled_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 803 if (!$fh) { 804 wp_delete_file($assembled_path); 805 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Could not open CSV file.'), 500); 806 } 807 $headers = fgetcsv($fh); 808 if (empty($headers)) { 809 fclose($fh); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 810 wp_delete_file($assembled_path); 811 wp_send_json(array('code' => 400, 'error' => 'EMPTY_FILE', 'message' => 'The CSV file contains no data.'), 400); 812 } 813 foreach ($headers as $header) { 814 $name = (string) $header; 815 $columns[] = array('name' => $name, 'label' => $name); 816 } 817 while (($csv_row = fgetcsv($fh)) !== false) { 818 if (count($sample_rows) < $sample_limit) { 819 $assoc = array(); 820 foreach ($headers as $idx => $h) { 821 $assoc[(string) $h] = isset($csv_row[$idx]) ? $csv_row[$idx] : ''; 822 } 823 $sample_rows[] = $assoc; 824 } 825 $total_rows++; 826 } 827 fclose($fh); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 828 } elseif ($extension === 'json') { 829 $raw_content = file_get_contents($assembled_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 830 $decoded = json_decode($raw_content, true); 831 if (!is_array($decoded) || empty($decoded)) { 832 wp_delete_file($assembled_path); 833 wp_send_json(array('code' => 400, 'error' => 'EMPTY_FILE', 'message' => 'The JSON file contains no data.'), 400); 834 } 835 $first_item = reset($decoded); 836 if (!is_array($first_item)) { 837 wp_delete_file($assembled_path); 838 wp_send_json(array('code' => 400, 'error' => 'INVALID_FORMAT', 'message' => 'JSON must be an array of objects.'), 400); 839 } 840 $headers = array_keys($first_item); 841 foreach ($headers as $key) { 842 $name = (string) $key; 843 $columns[] = array('name' => $name, 'label' => $name); 844 } 845 // Normalize JSON to the same streamable CSV cache that xlsx/xls produce so 846 // import_rows_batch never re-decodes the whole file on every batch (it just 847 // streams the CSV). Falls back to raw JSON if the cache can't be written. 848 $csv_cache_path = $assembled_dir . $session_id . '_cache.csv'; 849 $fh_out = fopen($csv_cache_path, 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 850 if ($fh_out) { 851 fputcsv($fh_out, $headers); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 852 $actual_rows = 0; 853 foreach ($decoded as $item) { 854 if (!is_array($item)) continue; 855 $padded = array(); 856 $assoc = array(); 857 foreach ($headers as $h) { 858 $cell = isset($item[$h]) ? $item[$h] : ''; 859 // Flatten nested values so the CSV cell stays scalar; import re-keys by header. 860 if (is_array($cell)) { 861 $cell = wp_json_encode($cell); 862 } 863 $padded[] = $cell; 864 $assoc[(string) $h] = $cell; 865 } 866 fputcsv($fh_out, $padded); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 867 if (count($sample_rows) < $sample_limit) { 868 $sample_rows[] = $assoc; 869 } 870 $actual_rows++; 871 } 872 fclose($fh_out); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 873 wp_delete_file($assembled_path); 874 $assembled_path = $csv_cache_path; 875 $extension = 'csv'; 876 $total_rows = $actual_rows; 877 } else { 878 $total_rows = count($decoded); 879 } 880 } 881 882 // Sniff a data type per column from the sample so the upload-step UI can 883 // pre-fill the data-type selectors. Pure inference; nothing is persisted yet. 884 $sniffed_types = vtables_sniff_column_types($headers, $sample_rows); 885 foreach ($columns as &$col_ref) { 886 $cname = isset($col_ref['name']) ? (string) $col_ref['name'] : ''; 887 $col_ref['dataType'] = isset($sniffed_types[$cname]) ? $sniffed_types[$cname] : 'text'; 888 } 889 unset($col_ref); 890 891 // Determine original table_type. The map doubles as a whitelist; rows 892 // imported before xls/xlsx were split carry the legacy merged value 'excel'. 893 $type_map = array('xlsx' => 'xlsx', 'xls' => 'xls', 'csv' => 'csv', 'json' => 'json'); 894 $original_type = isset($type_map[$original_extension]) ? $type_map[$original_extension] : 'csv'; 895 896 // Deduplicate title. 897 global $wpdb; 898 $vtables_table = $wpdb->prefix . 'vtables'; 899 900 $title = vtables_generate_unique_title($wpdb, $vtables_table, $title); 901 902 $source = wp_json_encode(array('columns' => $columns, 'rows' => array())); 903 904 if ($source === false) { 905 wp_delete_file($assembled_path); 906 wp_send_json(array('code' => 500, 'error' => 'ENCODE_ERROR', 'message' => 'Failed to encode table structure.'), 500); 907 } 908 909 $result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 910 $vtables_table, 911 array( 912 'title' => $title, 913 'source' => $source, 914 'table_type' => 'importing', 915 'settings' => vtables_filter_settings_for_free( 916 vtables_seed_visible_columns(!empty($settings) ? $settings : '{}', $columns), 917 '{}' 918 ), 919 'file_size' => $file_size, 920 'plan' => 'free', 110 921 ) 111 922 ); 112 923 113 vtables_maybe_send_nocache_headers($rest_response); 114 115 return $rest_response; 116 } 117 118 119 /** 120 * CREATE TABLE. 121 */ 122 add_action( 'rest_api_init', function () { 123 register_rest_route( 'vtables/v1', '/tables', array( 124 'methods' => 'POST', 125 'callback' => 'vtables_create_record_handler', 126 'permission_callback' => 'vtables_admin_permission_callback', 127 ) ); 128 } ); 129 130 function vtables_create_record_handler( WP_REST_Request $data ) { 924 if ($result === false) { 925 wp_delete_file($assembled_path); 926 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to create table record.'), 500); 927 } 928 929 $table_id = $wpdb->insert_id; 930 931 set_transient( 932 'vtables_import_' . $table_id, 933 array( 934 'session_id' => $session_id, 935 'file_path' => $assembled_path, 936 'file_ext' => $extension, 937 'total_rows' => $total_rows, 938 'imported_rows' => 0, 939 'original_type' => $original_type_override ?: $original_type, 940 'headers' => $headers, 941 // vTables Pro holds the import here until the user picks indexed 942 // columns for server-side sort/filter; there is no such step here. 943 'config_pending' => false, 944 'index_map' => array(), 945 ), 946 DAY_IN_SECONDS 947 ); 948 949 wp_send_json(array( 950 'table_id' => $table_id, 951 'title' => $title, 952 'total_rows' => $total_rows, 953 'columns' => $columns, 954 'config_pending' => false, 955 )); 956 } 957 958 959 /** 960 * IMPORT ROWS BATCH. 961 * Reads a batch of rows from the assembled file and appends them to the DB. 962 */ 963 add_action('wp_ajax_vtables_import_rows_batch', 'vtables_import_rows_batch_handler'); 964 function vtables_import_rows_batch_handler() { 965 vtables_require_admin_ajax(); 966 967 $table_id = isset($_POST['table_id']) ? intval($_POST['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 968 if (!$table_id) { 969 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id.'), 400); 970 } 971 972 $transient = get_transient('vtables_import_' . $table_id); 973 if (!$transient) { 974 wp_send_json(array('code' => 404, 'error' => 'IMPORT_NOT_FOUND', 'message' => 'Import session not found or expired.'), 404); 975 } 976 977 // Hold off until the upload-step index/type config is persisted (or skipped). 978 // The poller can call this immediately after finalize; returning early keeps 979 // it harmless and lets the user pick indexed columns before rows are written. 980 if (!empty($transient['config_pending'])) { 981 wp_send_json(array( 982 'imported_rows' => intval($transient['imported_rows']), 983 'total_rows' => intval($transient['total_rows']), 984 'done' => false, 985 'config_pending' => true, 986 )); 987 } 988 989 $file_path = $transient['file_path']; 990 $file_ext = $transient['file_ext']; 991 $total_rows = intval($transient['total_rows']); 992 $imported_rows = intval($transient['imported_rows']); 993 $original_type = $transient['original_type']; 994 $headers = isset($transient['headers']) ? $transient['headers'] : array(); 995 // Resolved column -> {type, slot} map (from persist_index_config). Empty = no 996 // indexed columns; slot sub-columns stay NULL for this table. 997 $index_map = isset($transient['index_map']) && is_array($transient['index_map']) ? $transient['index_map'] : array(); 998 999 // Guard: headers must be present in the transient. 1000 if (empty($headers)) { 1001 wp_send_json(array('code' => 500, 'error' => 'MISSING_HEADERS', 'message' => 'Import metadata is incomplete. Please restart the import.'), 500); 1002 } 1003 1004 $batch_size = 500; 1005 131 1006 global $wpdb; 132 $table_name = $wpdb->prefix . 'vtables'; 133 $title = $data['title']; 134 $source = json_encode($data['source']); 135 $file_size = $data['file_size']; 136 $external_source = $data['external_source']; 137 $settings = json_encode($data['settings']); 138 $table_type = $data['table_type']; 139 $woocommerce_categories = json_encode($data['woocommerce_categories']); 140 141 if ($title) { 142 $query = $wpdb->prepare( 143 "SELECT * FROM $table_name WHERE title = %s", 144 addslashes($title) 1007 $vtables_table = $wpdb->prefix . 'vtables'; 1008 $source_table = $wpdb->prefix . 'vtables_source'; 1009 1010 $batch_rows = array(); 1011 1012 if ($file_ext === 'xlsx') { 1013 $xlsx = \Shuchkin\SimpleXLSX::parse($file_path); 1014 if (!$xlsx) { 1015 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xlsx file.'), 500); 1016 } 1017 $all_rows = $xlsx->rows(); 1018 array_shift($all_rows); // remove header row 1019 $slice = array_slice($all_rows, $imported_rows, $batch_size); 1020 foreach ($slice as $raw_row) { 1021 $non_empty = array_filter($raw_row, function($v) { return $v !== '' && $v !== null; }); 1022 if (empty($non_empty)) continue; 1023 $row_data = array(); 1024 foreach ($headers as $idx => $header) { 1025 $row_data[$header] = isset($raw_row[$idx]) ? $raw_row[$idx] : ''; 1026 } 1027 $batch_rows[] = $row_data; 1028 } 1029 } elseif ($file_ext === 'xls') { 1030 $xls = \Shuchkin\SimpleXLS::parse($file_path); 1031 if (!$xls) { 1032 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Failed to parse .xls file.'), 500); 1033 } 1034 $all_rows = $xls->rows(); 1035 array_shift($all_rows); 1036 $slice = array_slice($all_rows, $imported_rows, $batch_size); 1037 foreach ($slice as $raw_row) { 1038 $non_empty = array_filter($raw_row, function($v) { return $v !== '' && $v !== null; }); 1039 if (empty($non_empty)) continue; 1040 $row_data = array(); 1041 foreach ($headers as $idx => $header) { 1042 $row_data[$header] = isset($raw_row[$idx]) ? $raw_row[$idx] : ''; 1043 } 1044 $batch_rows[] = $row_data; 1045 } 1046 } elseif ($file_ext === 'csv') { 1047 $fh = fopen($file_path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 1048 if (!$fh) { 1049 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Could not open CSV file.'), 500); 1050 } 1051 fgetcsv($fh); // skip header 1052 $current_line = 0; 1053 while (($csv_row = fgetcsv($fh)) !== false) { 1054 if ($current_line < $imported_rows) { 1055 $current_line++; 1056 continue; 1057 } 1058 if ($current_line >= $imported_rows + $batch_size) { 1059 break; 1060 } 1061 $non_empty = array_filter($csv_row, function($v) { return $v !== '' && $v !== null; }); 1062 if (!empty($non_empty)) { 1063 $row_data = array(); 1064 foreach ($headers as $idx => $header) { 1065 $row_data[$header] = isset($csv_row[$idx]) ? $csv_row[$idx] : ''; 1066 } 1067 $batch_rows[] = $row_data; 1068 } 1069 $current_line++; 1070 } 1071 fclose($fh); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 1072 } elseif ($file_ext === 'json') { 1073 $raw_content = file_get_contents($file_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents 1074 $decoded = json_decode($raw_content, true); 1075 if (!is_array($decoded)) { 1076 wp_send_json(array('code' => 500, 'error' => 'PARSE_ERROR', 'message' => 'Could not parse JSON file.'), 500); 1077 } 1078 $slice = array_slice($decoded, $imported_rows, $batch_size); 1079 foreach ($slice as $item) { 1080 if (!is_array($item)) continue; 1081 $row_data = array(); 1082 foreach ($headers as $header) { 1083 $row_data[$header] = isset($item[$header]) ? $item[$header] : ''; 1084 } 1085 $batch_rows[] = $row_data; 1086 } 1087 } 1088 1089 // Insert each row of the batch as its own record in wp_vtables_source. Each 1090 // INSERT packet only carries this batch (~500 small rows) and the largest single 1091 // value the server handles is one row — never the full accumulated dataset — so 1092 // the max_allowed_packet ceiling that broke the old CONCAT-into-one-cell approach 1093 // no longer applies. row_index stays contiguous per table_id. The 10 typed slot 1094 // sub-columns are populated from the resolved index map so indexed columns get 1095 // fast index-backed sort/filter (NULL when unset / uncoercible). See Phase C/D. 1096 $slot_cols = vtables_slot_columns(); 1097 $col_list = array_merge(array('table_id', 'row_index', 'row_json', 'search_blob'), $slot_cols); 1098 $values = array(); 1099 $params = array(); 1100 $i = 0; 1101 foreach (array_values($batch_rows) as $row) { 1102 $row_json = wp_json_encode($row); 1103 if ($row_json === false) { 1104 continue; 1105 } 1106 $slot_values = vtables_row_index_values($row, $index_map); 1107 1108 $placeholders = array('%d', '%d', '%s', '%s'); 1109 $params[] = $table_id; 1110 $params[] = $imported_rows + $i; 1111 $params[] = $row_json; 1112 $params[] = vtables_row_search_blob($row); 1113 foreach ($slot_cols as $slot_col) { 1114 if (!isset($slot_values[$slot_col]) || $slot_values[$slot_col] === null) { 1115 $placeholders[] = 'NULL'; 1116 } else { 1117 $placeholders[] = '%s'; 1118 $params[] = $slot_values[$slot_col]; 1119 } 1120 } 1121 $values[] = '(' . implode(', ', $placeholders) . ')'; 1122 $i++; 1123 } 1124 1125 $batch_count = count($values); 1126 $new_imported = $imported_rows + $batch_count; 1127 $done = $new_imported >= $total_rows; 1128 1129 if ($batch_count > 0) { 1130 $sql = "INSERT INTO {$source_table} (" . implode(', ', $col_list) . ") VALUES " . implode(', ', $values); 1131 $wpdb->query( $wpdb->prepare($sql, $params) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql holds only fixed column names and placeholders; every value goes through prepare() 1132 if ($wpdb->last_error) { 1133 vtables_send_db_error(); 1134 } 1135 } 1136 1137 // Flip table_type (importing -> original) when done; always bump updated_at. 1138 // source stays {columns:[...], rows:[]} on wp_vtables; rows live in wp_vtables_source. 1139 $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1140 $vtables_table, 1141 array('table_type' => $done ? $original_type : 'importing', 'updated_at' => current_time('mysql')), 1142 array('id' => $table_id), 1143 array('%s', '%s'), 1144 array('%d') 1145 ); 1146 1147 if ($done) { 1148 wp_delete_file($file_path); 1149 delete_transient('vtables_import_' . $table_id); 1150 } else { 1151 $transient['imported_rows'] = $new_imported; 1152 set_transient('vtables_import_' . $table_id, $transient, DAY_IN_SECONDS); 1153 } 1154 1155 wp_send_json(array( 1156 'imported_rows' => $new_imported, 1157 'total_rows' => $total_rows, 1158 'done' => $done, 1159 )); 1160 } 1161 1162 1163 /** 1164 * CANCEL IMPORT. 1165 * Deletes the in-progress table, the assembled file, and the import transient. 1166 */ 1167 add_action('wp_ajax_vtables_cancel_import', 'vtables_cancel_import_handler'); 1168 function vtables_cancel_import_handler() { 1169 vtables_require_admin_ajax(); 1170 1171 $table_id = isset($_POST['table_id']) ? intval($_POST['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1172 if (!$table_id) { 1173 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id.'), 400); 1174 } 1175 1176 // Only an import in progress can be cancelled; finished tables go through 1177 // delete_table. 1178 $meta = vtables_table_meta($table_id); 1179 if (!$meta || $meta->table_type !== 'importing') { 1180 wp_send_json(array('code' => 409, 'error' => 'NOT_IMPORTING', 'message' => 'This table is not being imported.'), 409); 1181 } 1182 1183 $transient = get_transient('vtables_import_' . $table_id); 1184 if ($transient) { 1185 // Remove assembled file. 1186 if (!empty($transient['file_path']) && file_exists($transient['file_path'])) { 1187 wp_delete_file($transient['file_path']); 1188 } 1189 // Remove chunk dir if still present (upload was cancelled before finalize). 1190 if (!empty($transient['session_id']) && preg_match('/^[0-9a-f\-]{36}$/i', $transient['session_id'])) { 1191 $chunk_dir = vtables_uploads_dir() . 'chunks/' . $transient['session_id'] . '/'; 1192 if (is_dir($chunk_dir)) { 1193 vtables_remove_directory($chunk_dir); 1194 } 1195 } 1196 delete_transient('vtables_import_' . $table_id); 1197 } 1198 1199 global $wpdb; 1200 $vtables_table = $wpdb->prefix . 'vtables'; 1201 $wpdb->delete($vtables_table, array('id' => $table_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1202 $wpdb->delete($wpdb->prefix . 'vtables_source', array('table_id' => $table_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1203 1204 wp_send_json(array('success' => true)); 1205 } 1206 1207 1208 /** 1209 * CANCEL CHUNK UPLOAD. 1210 * Removes the chunk directory for an in-progress (pre-finalize) upload session. 1211 */ 1212 add_action('wp_ajax_vtables_cancel_chunk_upload', 'vtables_cancel_chunk_upload_handler'); 1213 function vtables_cancel_chunk_upload_handler() { 1214 vtables_require_admin_ajax(); 1215 1216 $session_id = isset($_POST['session_id']) ? sanitize_text_field(wp_unslash($_POST['session_id'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification 1217 1218 if (!preg_match('/^[0-9a-f\-]{36}$/i', $session_id)) { 1219 wp_send_json(array('code' => 400, 'error' => 'INVALID_PARAMS', 'message' => 'Invalid session ID.'), 400); 1220 } 1221 1222 $chunk_dir = vtables_uploads_dir() . 'chunks/' . $session_id . '/'; 1223 1224 if (is_dir($chunk_dir)) { 1225 vtables_remove_directory($chunk_dir); 1226 } 1227 1228 wp_send_json(array('success' => true)); 1229 } 1230 1231 1232 /** 1233 * QUERY ROWS (server-mode paginated read). 1234 */ 1235 add_action('wp_ajax_vtables_query_rows', 'vtables_query_rows_handler'); 1236 add_action('wp_ajax_nopriv_vtables_query_rows', 'vtables_query_rows_handler'); 1237 function vtables_query_rows_handler() { 1238 if (!defined('DOING_AJAX') || !DOING_AJAX) { 1239 wp_send_json(array('code' => 400, 'error' => 'INVALID_REQUEST', 'message' => 'Invalid request.'), 400); 1240 } 1241 1242 $table_id = isset($_GET['table_id']) ? intval($_GET['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1243 if (!$table_id) { 1244 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id.'), 400); 1245 } 1246 1247 vtables_reject_pro_table($table_id); 1248 1249 // Plain pagination only: search, sorting and filtering are vTables Pro features. 1250 $result = vtables_query_source_rows(array( 1251 'table_id' => $table_id, 1252 'page' => isset($_GET['page']) ? intval($_GET['page']) : 1, // phpcs:ignore WordPress.Security.NonceVerification 1253 'per_page' => isset($_GET['per_page']) ? intval($_GET['per_page']) : vtables_default_per_page(), // phpcs:ignore WordPress.Security.NonceVerification 1254 'after_row_index' => isset($_GET['after_row_index']) && $_GET['after_row_index'] !== '' ? intval($_GET['after_row_index']) : '', // phpcs:ignore WordPress.Security.NonceVerification 1255 )); 1256 1257 global $wpdb; 1258 if ($wpdb->last_error) { 1259 vtables_send_db_error(); 1260 } 1261 1262 wp_send_json(array('rows' => $result['rows'], 'total' => $result['total'])); 1263 } 1264 1265 1266 /** 1267 * UPDATE ROW (server-mode single-row edit). 1268 */ 1269 add_action('wp_ajax_vtables_update_row', 'vtables_update_row_handler'); 1270 function vtables_update_row_handler() { 1271 vtables_require_admin_ajax(); 1272 1273 $table_id = isset($_POST['table_id']) ? intval($_POST['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1274 $vt_id = isset($_POST['vt_id']) ? intval($_POST['vt_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1275 $row_raw = isset($_POST['row']) ? wp_unslash($_POST['row']) : ''; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, decoded below 1276 1277 if (!$table_id || !$vt_id) { 1278 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id or vt_id.'), 400); 1279 } 1280 1281 vtables_reject_pro_table($table_id); 1282 1283 $row = json_decode($row_raw, true); 1284 if (!is_array($row)) { 1285 wp_send_json(array('code' => 400, 'error' => 'INVALID_ROW', 'message' => 'Invalid row payload.'), 400); 1286 } 1287 unset($row['vt_id'], $row['vt_row_index']); 1288 1289 global $wpdb; 1290 $source_table = $wpdb->prefix . 'vtables_source'; 1291 $row_json = wp_json_encode($row); 1292 1293 // Refresh search_blob AND the typed slot sub-columns so search and indexed 1294 // sort/filter stay consistent with the edited cell values. 1295 $index_map = vtables_table_index_map($table_id); 1296 $slot_values = vtables_row_index_values($row, $index_map); 1297 1298 $data = array('row_json' => $row_json, 'search_blob' => vtables_row_search_blob($row)); 1299 $formats = array('%s', '%s'); 1300 foreach (vtables_slot_columns() as $slot_col) { 1301 $data[$slot_col] = isset($slot_values[$slot_col]) ? $slot_values[$slot_col] : null; 1302 $formats[] = '%s'; 1303 } 1304 1305 $result = $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1306 $source_table, 1307 $data, 1308 array('id' => $vt_id, 'table_id' => $table_id), 1309 $formats, 1310 array('%d', '%d') 1311 ); 1312 1313 if ($result === false) { 1314 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to update row.'), 500); 1315 } 1316 1317 // Cell values changed but the row count didn't; only filtered/searched totals 1318 // could shift, so drop the cached counts for this table to stay correct. 1319 vtables_invalidate_count_cache($table_id); 1320 1321 wp_send_json(array('vt_id' => $vt_id)); 1322 } 1323 1324 1325 /** 1326 * INSERT ROW (server-mode add row). 1327 */ 1328 1329 /** 1330 * Named-lock key serializing anchored inserts per table. GET_LOCK names are 1331 * global to the MySQL server and capped at 64 chars, so scope per 1332 * install+blog+table through a hash. The same name vTables Pro uses, since 1333 * both write rows of the same tables. 1334 */ 1335 function vtables_row_insert_lock_name($table_id) { 1336 global $wpdb; 1337 $db = defined('DB_NAME') ? DB_NAME : ''; 1338 return 'vtables_pro_rows:' . md5($db . '|' . $wpdb->prefix . '|' . (int) $table_id); 1339 } 1340 1341 add_action('wp_ajax_vtables_insert_row', 'vtables_insert_row_handler'); 1342 function vtables_insert_row_handler() { 1343 vtables_require_admin_ajax(); 1344 1345 $table_id = isset($_POST['table_id']) ? intval($_POST['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1346 $after_index = isset($_POST['after_index']) ? intval($_POST['after_index']) : -1; // phpcs:ignore WordPress.Security.NonceVerification 1347 $anchor_vt_id = isset($_POST['anchor_vt_id']) ? intval($_POST['anchor_vt_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1348 $position = isset($_POST['position']) && sanitize_text_field(wp_unslash($_POST['position'])) === 'before' ? 'before' : 'after'; // phpcs:ignore WordPress.Security.NonceVerification 1349 $row_raw = isset($_POST['row']) ? wp_unslash($_POST['row']) : ''; // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, decoded below 1350 1351 if (!$table_id) { 1352 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id.'), 400); 1353 } 1354 1355 // Bail before any write: wp_vtables_source has no FK, so an insert against a 1356 // deleted/never-existing table_id would orphan the row permanently. 1357 if (!vtables_table_exists($table_id)) { 1358 wp_send_json(array('code' => 404, 'error' => 'TABLE_NOT_FOUND', 'message' => 'Table not found.'), 404); 1359 } 1360 1361 vtables_reject_pro_table($table_id); 1362 1363 $row = json_decode($row_raw, true); 1364 if (!is_array($row)) { 1365 $row = array(); 1366 } 1367 unset($row['vt_id'], $row['vt_row_index']); 1368 1369 global $wpdb; 1370 $source_table = $wpdb->prefix . 'vtables_source'; 1371 1372 // Anchored insert: resolve the target index from the anchor row, shift every 1373 // row at/after it up by one and take the freed slot. Keeps row_index unique 1374 // and ordered — keyset pagination in vtables_query_source_rows 1375 // (WHERE row_index > %d) breaks on duplicates, which is why the old sparse 1376 // "after_index + 1" placement misplaced rows. The shift and the insert are 1377 // two statements, so serialize per table with a named lock (session-scoped, 1378 // released on request shutdown, so every wp_send_json exit path is covered). 1379 if ($anchor_vt_id > 0) { 1380 $lock = vtables_row_insert_lock_name($table_id); 1381 $acquired = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1382 $wpdb->prepare('SELECT GET_LOCK(%s, %d)', $lock, 10) 145 1383 ); 146 $row_exists = $wpdb->get_results($query); 147 $title = $row_exists ? $title . '-copy' : $title; 148 } 149 150 $results = $wpdb->insert($table_name, 151 array( 152 'title' => $title, 153 'source' => $source, 154 'file_size' => $file_size, 155 'external_source' => $external_source, 156 'settings' => $settings, 157 'table_type' => $table_type, 158 'woocommerce_categories' => $woocommerce_categories, 159 ) 1384 if ($acquired !== 1) { 1385 wp_send_json(array('code' => 503, 'error' => 'ROWS_BUSY', 'message' => 'Another row operation is in progress. Please retry.'), 503); 1386 } 1387 register_shutdown_function(function () use ($wpdb, $lock) { 1388 $wpdb->query($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1389 }); 1390 1391 $anchor_index = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1392 $wpdb->prepare( "SELECT row_index FROM {$wpdb->prefix}vtables_source WHERE id = %d AND table_id = %d", $anchor_vt_id, $table_id ) 1393 ); 1394 if ($anchor_index === null) { 1395 wp_send_json(array('code' => 404, 'error' => 'ANCHOR_NOT_FOUND', 'message' => 'Anchor row not found.'), 404); 1396 } 1397 1398 $new_index = (int) $anchor_index + ($position === 'after' ? 1 : 0); 1399 $shifted = $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1400 $wpdb->prepare( "UPDATE {$wpdb->prefix}vtables_source SET row_index = row_index + 1 WHERE table_id = %d AND row_index >= %d", $table_id, $new_index ) 1401 ); 1402 if ($shifted === false) { 1403 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to insert row.'), 500); 1404 } 1405 } elseif ($after_index >= 0) { 1406 // Legacy sparse positioning (no anchor supplied): place just after the 1407 // requested index; ORDER BY row_index, id breaks the tie. 1408 $new_index = $after_index + 1; 1409 } else { 1410 $max_index = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1411 $wpdb->prepare( "SELECT MAX(row_index) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $table_id ) 1412 ); 1413 $new_index = ($max_index === null) ? 0 : ((int) $max_index + 1); 1414 } 1415 1416 // Populate the typed slot sub-columns for indexed columns on insert too. 1417 $index_map = vtables_table_index_map($table_id); 1418 $slot_values = vtables_row_index_values($row, $index_map); 1419 1420 $data = array( 1421 'table_id' => $table_id, 1422 'row_index' => $new_index, 1423 'row_json' => wp_json_encode($row), 1424 'search_blob' => vtables_row_search_blob($row), 160 1425 ); 161 162 $response = array(); 163 $response['id'] = $wpdb->insert_id; 164 165 $rest_response = new WP_REST_Response( 166 array( 167 'success' => !!$results, 168 'data' => $response, 169 ) 1426 $formats = array('%d', '%d', '%s', '%s'); 1427 foreach (vtables_slot_columns() as $slot_col) { 1428 $data[$slot_col] = isset($slot_values[$slot_col]) ? $slot_values[$slot_col] : null; 1429 $formats[] = '%s'; 1430 } 1431 1432 $result = $wpdb->insert($source_table, $data, $formats); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1433 1434 if ($result === false) { 1435 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to insert row.'), 500); 1436 } 1437 1438 vtables_invalidate_count_cache($table_id); 1439 1440 wp_send_json(array('vt_id' => $wpdb->insert_id, 'row_index' => $new_index)); 1441 } 1442 1443 1444 /** 1445 * DELETE ROW (server-mode delete row). 1446 */ 1447 add_action('wp_ajax_vtables_delete_row', 'vtables_delete_row_handler'); 1448 function vtables_delete_row_handler() { 1449 vtables_require_admin_ajax(); 1450 1451 $table_id = isset($_POST['table_id']) ? intval($_POST['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1452 $vt_id = isset($_POST['vt_id']) ? intval($_POST['vt_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1453 1454 if (!$table_id || !$vt_id) { 1455 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id or vt_id.'), 400); 1456 } 1457 1458 vtables_reject_pro_table($table_id); 1459 1460 global $wpdb; 1461 $source_table = $wpdb->prefix . 'vtables_source'; 1462 $result = $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1463 $source_table, 1464 array('id' => $vt_id, 'table_id' => $table_id), 1465 array('%d', '%d') 170 1466 ); 171 1467 172 vtables_maybe_send_nocache_headers($rest_response); 173 174 return $rest_response; 175 } 176 177 178 /** 179 * SAVE TABLE CHANGES. 180 */ 181 add_action( 'rest_api_init', function () { 182 register_rest_route( 'vtables/v1', '/source', array( 183 'methods' => 'PUT', 184 'callback' => 'vtables_save_table_changes_handler', 185 'permission_callback' => 'vtables_admin_permission_callback', 186 ) ); 187 } ); 188 189 function vtables_save_table_changes_handler( WP_REST_Request $data ) { 1468 if ($result === false) { 1469 wp_send_json(array('code' => 500, 'error' => 'DB_ERROR', 'message' => 'Failed to delete row.'), 500); 1470 } 1471 1472 vtables_invalidate_count_cache($table_id); 1473 1474 wp_send_json(array('vt_id' => $vt_id)); 1475 } 1476 1477 1478 /** 1479 * HELPER: the table's stored columns as name/label pairs. 1480 * Names key row_json; export needs the labels too, because that is what the 1481 * UI shows as the header. 1482 */ 1483 if (!function_exists('vtables_table_column_pairs')) { 1484 function vtables_table_column_pairs($table_id) { 1485 global $wpdb; 1486 $source = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1487 $wpdb->prepare( "SELECT source FROM {$wpdb->prefix}vtables WHERE id = %d", $table_id ) 1488 ); 1489 $decoded = json_decode($source, true); 1490 $pairs = array(); 1491 if (is_array($decoded) && isset($decoded['columns']) && is_array($decoded['columns'])) { 1492 foreach ($decoded['columns'] as $col) { 1493 if (!isset($col['name'])) { 1494 continue; 1495 } 1496 $name = (string) $col['name']; 1497 $pairs[] = array( 1498 'name' => $name, 1499 'label' => isset($col['label']) && $col['label'] !== '' ? (string) $col['label'] : $name, 1500 ); 1501 } 1502 } 1503 return $pairs; 1504 } 1505 } 1506 1507 /** 1508 * HELPER: flatten one cell value for a CSV field. 1509 * Cells are not always scalars — json-typed cells hold objects/arrays and 1510 * booleans. Handing those to fputcsv() emits the literal "Array" plus a PHP 1511 * notice, so encode them instead. 1512 */ 1513 if (!function_exists('vtables_export_scalarize')) { 1514 function vtables_export_scalarize($value) { 1515 if (is_null($value)) { 1516 return ''; 1517 } 1518 if (is_bool($value)) { 1519 return $value ? 'true' : 'false'; 1520 } 1521 if (is_scalar($value)) { 1522 return (string) $value; 1523 } 1524 return (string) wp_json_encode($value); 1525 } 1526 } 1527 1528 /** 1529 * Hard ceiling of the xlsx sheet format itself (rows per worksheet). 1530 */ 1531 if (!defined('VTABLES_XLSX_MAX_ROWS')) { 1532 define('VTABLES_XLSX_MAX_ROWS', 1048576); 1533 } 1534 1535 /** 1536 * EXPORT ROWS (stream all rows for a table without buffering the whole dataset). 1537 * 1538 * Always exports the full, unfiltered table — the active search/filter/sort of 1539 * the browsing session is deliberately not applied. 1540 */ 1541 add_action('wp_ajax_vtables_export_rows', 'vtables_export_rows_handler'); 1542 function vtables_export_rows_handler() { 1543 vtables_require_admin_ajax(); 1544 1545 $table_id = isset($_GET['table_id']) ? intval($_GET['table_id']) : 0; // phpcs:ignore WordPress.Security.NonceVerification 1546 $format = isset($_GET['format']) ? sanitize_text_field(wp_unslash($_GET['format'])) : 'csv'; // phpcs:ignore WordPress.Security.NonceVerification 1547 if (!in_array($format, array('csv', 'json', 'xlsx'), true)) { 1548 $format = 'csv'; 1549 } 1550 1551 if (!$table_id) { 1552 wp_send_json(array('code' => 400, 'error' => 'MISSING_REQUIRED_FIELDS', 'message' => 'Missing table_id.'), 400); 1553 } 1554 190 1555 global $wpdb; 191 $table_name = $wpdb->prefix . 'vtables'; 192 $id = $data['id']; 193 $title = $data['title']; 194 $source = $data['source']; 195 $file_size = $data['fileSize']; 196 $sourceJSON = json_encode($data['source']); 197 198 // Update only title 199 if ($title && empty($source)) { 200 $results = $wpdb->update( 201 $table_name, 202 array( 203 'title' => $title, 204 'updated_at' => date('Y-m-d h:i:s'), 205 ), 206 array( 'id' => $data['id'] ), 1556 $table = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1557 $wpdb->prepare( "SELECT id, title, table_type, source FROM {$wpdb->prefix}vtables WHERE id = %d", $table_id ) 1558 ); 1559 1560 if (!$table) { 1561 wp_send_json(array('code' => 404, 'error' => 'NOT_FOUND', 'message' => 'Table not found.'), 404); 1562 } 1563 1564 vtables_reject_pro_table($table_id); 1565 1566 // Everything that can refuse the request has to do so before a byte of the 1567 // download goes out, so the xlsx preconditions are checked up here. 1568 if ($format === 'xlsx') { 1569 if (!class_exists('ZipArchive')) { 1570 wp_send_json(array('code' => 501, 'error' => 'ZIP_UNAVAILABLE', 'message' => 'XLSX export needs the PHP zip extension. Use CSV instead.'), 501); 1571 } 1572 1573 $row_count = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1574 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $table_id ) 207 1575 ); 208 209 return new WP_REST_Response( 210 array( 211 'success' => !!$results, 212 'data' => $results, 213 ) 1576 // The sheet format itself caps out at 1,048,576 rows (one goes to the 1577 // header). Truncating silently would hand back a plausible-looking but 1578 // incomplete file, so refuse and point at the formats that have no ceiling. 1579 if ($row_count > VTABLES_XLSX_MAX_ROWS - 1) { 1580 wp_send_json(array( 1581 'code' => 400, 1582 'error' => 'TOO_MANY_ROWS', 1583 'message' => sprintf('This table has %d rows; XLSX holds at most %d. Export as CSV or JSON instead.', $row_count, VTABLES_XLSX_MAX_ROWS - 1), 1584 ), 400); 1585 } 1586 } 1587 1588 // Match the filename the client-side exporter used before this endpoint 1589 // existed, so downloads stay recognizable. 1590 $basename = sanitize_file_name($table->title); 1591 if ($basename === '') { 1592 $basename = 'vtable-' . $table_id; 1593 } 1594 1595 // Streaming past a buffer would defeat the whole point (and blow memory on 1596 // large tables), so drop whatever WordPress/plugins have open. 1597 while (ob_get_level() > 0) { 1598 ob_end_clean(); 1599 } 1600 1601 $content_types = array( 1602 'json' => 'application/json; charset=utf-8', 1603 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 1604 'csv' => 'text/csv; charset=utf-8', 1605 ); 1606 1607 nocache_headers(); 1608 header('Content-Type: ' . $content_types[$format]); 1609 header('Content-Disposition: attachment; filename="' . $basename . '.' . $format . '"'); 1610 1611 $writer = vtables_export_writer_open($format); 1612 1613 vtables_export_stream_source_rows($table, $writer); 1614 1615 vtables_export_writer_close($writer); 1616 1617 wp_die(); 1618 } 1619 1620 /** 1621 * EXPORT: stream rows out of wp_vtables_source. 1622 * Legacy tables kept their rows inside the source blob and have no records 1623 * there, so fall back to that blob rather than emitting an empty file. 1624 */ 1625 function vtables_export_stream_source_rows($table, &$writer) { 1626 global $wpdb; 1627 $table_id = (int) $table->id; 1628 $pairs = vtables_table_column_pairs($table_id); 1629 1630 $has_records = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1631 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $table_id ) 1632 ) > 0; 1633 1634 $legacy_rows = array(); 1635 if (!$has_records) { 1636 $decoded = json_decode($table->source, true); 1637 if (is_array($decoded) && isset($decoded['rows']) && is_array($decoded['rows'])) { 1638 $legacy_rows = $decoded['rows']; 1639 } 1640 } 1641 1642 vtables_export_writer_header($writer, $pairs); 1643 1644 $batch = 1000; 1645 $offset = 0; 1646 1647 do { 1648 if ($has_records) { 1649 $records = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 1650 $wpdb->prepare( "SELECT row_json FROM {$wpdb->prefix}vtables_source WHERE table_id = %d ORDER BY row_index ASC, id ASC LIMIT %d OFFSET %d", $table_id, $batch, $offset ) 1651 ); 1652 } else { 1653 $records = array_map('wp_json_encode', array_slice($legacy_rows, $offset, $batch)); 1654 } 1655 1656 foreach ($records as $row_json) { 1657 if ($writer['format'] === 'json') { 1658 // Already-encoded JSON goes out verbatim — no decode/re-encode round trip. 1659 vtables_export_writer_row($writer, null, $pairs, $row_json); 1660 continue; 1661 } 1662 1663 $row = json_decode($row_json, true); 1664 if (!is_array($row)) { 1665 continue; 1666 } 1667 vtables_export_writer_row($writer, $row, $pairs); 1668 } 1669 1670 $offset += $batch; 1671 flush(); 1672 } while (count($records) === $batch); 1673 } 1674 1675 /** 1676 * EXPORT: one row -> the values for the column list, in column order. Without 1677 * a column list (columnless legacy sources) fall back to the row's own order. 1678 */ 1679 function vtables_export_row_values($row, $pairs) { 1680 if (empty($pairs)) { 1681 return array_values($row); 1682 } 1683 1684 $values = array(); 1685 foreach ($pairs as $pair) { 1686 $values[] = isset($row[$pair['name']]) ? $row[$pair['name']] : ''; 1687 } 1688 return $values; 1689 } 1690 1691 1692 /** 1693 * ============================================================================ 1694 * EXPORT WRITERS 1695 * One tiny state machine per output format, so the two row-producing streams 1696 * above stay format-agnostic. csv and json go straight to php://output; xlsx 1697 * builds on disk (see vtables_export_writer_close). 1698 * ============================================================================ 1699 */ 1700 1701 function vtables_export_writer_open($format) { 1702 $writer = array('format' => $format); 1703 1704 if ($format === 'json') { 1705 $writer['first'] = true; 1706 echo '['; 1707 return $writer; 1708 } 1709 1710 if ($format === 'csv') { 1711 $writer['out'] = fopen('php://output', 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 1712 return $writer; 1713 } 1714 1715 // xlsx: the sheet is the only large part, so it is written to a temp file as 1716 // rows arrive and zipped at the end — memory stays flat regardless of size. 1717 $writer['sheet_path'] = wp_tempnam('vtables-export-sheet'); 1718 $writer['sheet'] = fopen($writer['sheet_path'], 'w'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen 1719 $writer['row'] = 0; 1720 fwrite( // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 1721 $writer['sheet'], 1722 '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' 1723 . '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData>' 1724 ); 1725 return $writer; 1726 } 1727 1728 function vtables_export_writer_header(&$writer, $pairs) { 1729 if ($writer['format'] === 'json' || empty($pairs)) { 1730 return; 1731 } 1732 $labels = wp_list_pluck($pairs, 'label'); 1733 1734 if ($writer['format'] === 'csv') { 1735 fputcsv($writer['out'], $labels); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 1736 return; 1737 } 1738 1739 vtables_export_xlsx_row($writer, $labels); 1740 } 1741 1742 /** 1743 * @param array|null $row decoded row; may be null when $encoded is given. 1744 * @param array $pairs column list. 1745 * @param string|null $encoded pre-encoded JSON, to skip a decode/encode cycle. 1746 */ 1747 function vtables_export_writer_row(&$writer, $row, $pairs, $encoded = null) { 1748 if ($writer['format'] === 'json') { 1749 echo $writer['first'] ? '' : ','; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1750 echo $encoded !== null ? $encoded : wp_json_encode($row); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 1751 $writer['first'] = false; 1752 return; 1753 } 1754 1755 $values = vtables_export_row_values($row, $pairs); 1756 1757 if ($writer['format'] === 'csv') { 1758 fputcsv($writer['out'], array_map('vtables_export_scalarize', $values)); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 1759 return; 1760 } 1761 1762 vtables_export_xlsx_row($writer, $values); 1763 } 1764 1765 function vtables_export_writer_close(&$writer) { 1766 if ($writer['format'] === 'json') { 1767 echo ']'; 1768 return; 1769 } 1770 1771 if ($writer['format'] === 'csv') { 1772 fclose($writer['out']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 1773 return; 1774 } 1775 1776 fwrite($writer['sheet'], '</sheetData></worksheet>'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 1777 fclose($writer['sheet']); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose 1778 1779 $xlsx_path = wp_tempnam('vtables-export-book'); 1780 $zip = new ZipArchive(); 1781 if ($zip->open($xlsx_path, ZipArchive::OVERWRITE) === true) { 1782 foreach (vtables_export_xlsx_parts() as $name => $contents) { 1783 $zip->addFromString($name, $contents); 1784 } 1785 $zip->addFile($writer['sheet_path'], 'xl/worksheets/sheet1.xml'); 1786 $zip->close(); 1787 readfile($xlsx_path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile 1788 } 1789 1790 wp_delete_file($writer['sheet_path']); 1791 wp_delete_file($xlsx_path); 1792 } 1793 1794 1795 /** 1796 * XLSX: append one <row>. Values that are genuinely numeric become numeric 1797 * cells; everything else becomes an inline string — which is what lets the 1798 * sheet stream, since a shared-strings table would have to be held in memory 1799 * until the very last row. 1800 */ 1801 function vtables_export_xlsx_row(&$writer, $values) { 1802 $writer['row']++; 1803 $r = $writer['row']; 1804 1805 $xml = '<row r="' . $r . '">'; 1806 $c = 0; 1807 foreach ($values as $value) { 1808 $c++; 1809 $ref = vtables_export_xlsx_column_ref($c) . $r; 1810 1811 if (vtables_export_xlsx_is_numeric($value)) { 1812 $xml .= '<c r="' . $ref . '"><v>' . (0 + $value) . '</v></c>'; 1813 continue; 1814 } 1815 1816 $text = vtables_export_xlsx_text($value); 1817 if ($text === '') { 1818 continue; // an empty cell needs no element at all 1819 } 1820 // xml:space="preserve" or Excel eats leading/trailing whitespace. 1821 $xml .= '<c r="' . $ref . '" t="inlineStr"><is><t xml:space="preserve">' . $text . '</t></is></c>'; 1822 } 1823 $xml .= '</row>'; 1824 1825 fwrite($writer['sheet'], $xml); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite 1826 } 1827 1828 /** 1829 * XLSX: should this value be written as a number? 1830 * Only when the canonical numeric form round-trips — so "00601" (a zip code) 1831 * and "1e5" stay text rather than being silently rewritten by the spreadsheet. 1832 */ 1833 function vtables_export_xlsx_is_numeric($value) { 1834 if (is_int($value) || is_float($value)) { 1835 return is_finite((float) $value); 1836 } 1837 if (!is_string($value) || $value === '' || !is_numeric($value)) { 1838 return false; 1839 } 1840 return (string) (0 + $value) === $value; 1841 } 1842 1843 /** 1844 * XLSX: escape a value for XML. Control characters are not merely invalid 1845 * here — Excel rejects the whole workbook over one of them, so strip them. 1846 */ 1847 function vtables_export_xlsx_text($value) { 1848 $text = vtables_export_scalarize($value); 1849 // Byte-wise is safe: UTF-8 continuation bytes are all >= 0x80. 1850 $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $text); 1851 return htmlspecialchars($text, ENT_QUOTES | ENT_XML1, 'UTF-8'); 1852 } 1853 1854 /** 1855 * XLSX: 1 -> A, 26 -> Z, 27 -> AA, ... 1856 */ 1857 function vtables_export_xlsx_column_ref($index) { 1858 $ref = ''; 1859 while ($index > 0) { 1860 $index--; 1861 $ref = chr(65 + ($index % 26)) . $ref; 1862 $index = intdiv($index, 26); 1863 } 1864 return $ref; 1865 } 1866 1867 /** 1868 * XLSX: the fixed, tiny package parts. Everything variable lives in 1869 * xl/worksheets/sheet1.xml, which is added separately from its temp file. 1870 */ 1871 function vtables_export_xlsx_parts() { 1872 $ns_rel = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'; 1873 $decl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'; 1874 1875 return array( 1876 '[Content_Types].xml' => $decl 1877 . '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' 1878 . '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' 1879 . '<Default Extension="xml" ContentType="application/xml"/>' 1880 . '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' 1881 . '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' 1882 . '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>' 1883 . '</Types>', 1884 1885 '_rels/.rels' => $decl 1886 . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' 1887 . '<Relationship Id="rId1" Type="' . $ns_rel . '/officeDocument" Target="xl/workbook.xml"/>' 1888 . '</Relationships>', 1889 1890 'xl/workbook.xml' => $decl 1891 . '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="' . $ns_rel . '">' 1892 . '<sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets>' 1893 . '</workbook>', 1894 1895 'xl/_rels/workbook.xml.rels' => $decl 1896 . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' 1897 . '<Relationship Id="rId1" Type="' . $ns_rel . '/worksheet" Target="worksheets/sheet1.xml"/>' 1898 . '<Relationship Id="rId2" Type="' . $ns_rel . '/styles" Target="styles.xml"/>' 1899 . '</Relationships>', 1900 1901 // Minimal but complete: readers that assume a styles part exists (and index 1902 // style 0) would otherwise choke. 1903 'xl/styles.xml' => $decl 1904 . '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' 1905 . '<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>' 1906 . '<fills count="1"><fill><patternFill patternType="none"/></fill></fills>' 1907 . '<borders count="1"><border/></borders>' 1908 . '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>' 1909 . '<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>' 1910 . '</styleSheet>', 1911 ); 1912 } 1913 1914 1915 /** 1916 * HELPER: page size for server-mode reads. 1917 * Fixed: the rows-per-page setting (settings.bottom.initial.rowsPerPage) is a 1918 * vTables Pro feature, so every table pages by the same default a new table 1919 * starts with (INITIAL_SETTINGS in src/stores/main/constants.ts). 1920 */ 1921 if (!function_exists('vtables_default_per_page')) { 1922 function vtables_default_per_page() { 1923 return max(1, min(1000, (int) apply_filters('vtables_default_per_page', 10))); 1924 } 1925 } 1926 1927 /** 1928 * HELPER: hard upload size cap in bytes (10 MB). Enforced client- and server-side. 1929 */ 1930 if (!function_exists('vtables_max_upload_bytes')) { 1931 function vtables_max_upload_bytes() { 1932 return (int) apply_filters('vtables_max_upload_bytes', 10 * 1024 * 1024); 1933 } 1934 } 1935 1936 /** 1937 * HELPER: ordered list of the typed slot sub-columns on wp_vtables_source. 1938 * Slot k (1..5): idx_k_txt (VARCHAR) for text, idx_k_num (DECIMAL) for 1939 * number/date(epoch)/boolean(0|1). 1940 */ 1941 if (!function_exists('vtables_slot_columns')) { 1942 function vtables_slot_columns() { 1943 $cols = array(); 1944 for ($k = 1; $k <= 5; $k++) { 1945 $cols[] = "idx_{$k}_txt"; 1946 $cols[] = "idx_{$k}_num"; 1947 } 1948 return $cols; 1949 } 1950 } 1951 1952 /** 1953 * HELPER: coerce a raw cell value to the numeric representation for a data type, 1954 * or null when it can't be represented (so it sorts last and never throws). 1955 * Returns array('txt' => string|null, 'num' => string|null) — only one is used 1956 * per slot depending on the column's type, but both are computed cheaply. 1957 */ 1958 if (!function_exists('vtables_coerce_index_value')) { 1959 function vtables_coerce_index_value($value, $type) { 1960 if (is_array($value) || is_object($value)) { 1961 // Non-scalar values can't be indexed meaningfully. 1962 return array('txt' => null, 'num' => null); 1963 } 1964 $str = ($value === null) ? '' : (string) $value; 1965 $trimmed = trim($str); 1966 1967 switch ($type) { 1968 case 'number': 1969 if ($trimmed === '' || !is_numeric($trimmed)) { 1970 return array('txt' => null, 'num' => null); 1971 } 1972 return array('txt' => null, 'num' => (string) (0 + $trimmed)); 1973 1974 case 'date': 1975 if ($trimmed === '') { 1976 return array('txt' => null, 'num' => null); 1977 } 1978 // Numeric epoch passes straight through; otherwise parse a date string. 1979 if (is_numeric($trimmed)) { 1980 return array('txt' => null, 'num' => (string) (int) $trimmed); 1981 } 1982 $ts = strtotime($trimmed); 1983 if ($ts === false) { 1984 return array('txt' => null, 'num' => null); 1985 } 1986 return array('txt' => null, 'num' => (string) $ts); 1987 1988 case 'boolean': 1989 if ($trimmed === '') { 1990 return array('txt' => null, 'num' => null); 1991 } 1992 $low = strtolower($trimmed); 1993 if (in_array($low, array('1', 'true', 'yes', 'y', 'on'), true)) { 1994 return array('txt' => null, 'num' => '1'); 1995 } 1996 if (in_array($low, array('0', 'false', 'no', 'n', 'off'), true)) { 1997 return array('txt' => null, 'num' => '0'); 1998 } 1999 return array('txt' => null, 'num' => null); 2000 2001 case 'text': 2002 default: 2003 if ($str === '') { 2004 return array('txt' => null, 'num' => null); 2005 } 2006 // VARCHAR(191): truncate for indexing only (raw value stays in row_json). 2007 return array('txt' => function_exists('mb_substr') ? mb_substr($str, 0, 191) : substr($str, 0, 191), 'num' => null); 2008 } 2009 } 2010 } 2011 2012 /** 2013 * HELPER: build the slot sub-column values for one row from a resolved index map 2014 * ($map: column name -> array('type' => ..., 'slot' => 1..5)). 2015 * Returns array('idx_k_txt' => ..., 'idx_k_num' => ...) with null where unused. 2016 */ 2017 if (!function_exists('vtables_row_index_values')) { 2018 function vtables_row_index_values($row, $map) { 2019 $out = array(); 2020 if (!is_array($row) || !is_array($map) || empty($map)) { 2021 return $out; 2022 } 2023 foreach ($map as $col => $meta) { 2024 $slot = isset($meta['slot']) ? (int) $meta['slot'] : 0; 2025 $type = isset($meta['type']) ? (string) $meta['type'] : 'text'; 2026 if ($slot < 1 || $slot > 5) { 2027 continue; 2028 } 2029 $value = isset($row[$col]) ? $row[$col] : null; 2030 $coerced = vtables_coerce_index_value($value, $type); 2031 if ($type === 'text') { 2032 $out["idx_{$slot}_txt"] = $coerced['txt']; 2033 $out["idx_{$slot}_num"] = null; 2034 } else { 2035 $out["idx_{$slot}_txt"] = null; 2036 $out["idx_{$slot}_num"] = $coerced['num']; 2037 } 2038 } 2039 return $out; 2040 } 2041 } 2042 2043 /** 2044 * HELPER: does a table row exist in wp_vtables? Handlers that write rows into 2045 * wp_vtables_source must call this first — the source table has no FK back to 2046 * wp_vtables, so inserting against a stale/bogus table_id silently orphans the 2047 * row where nothing but a manual sweep will find it. 2048 */ 2049 if (!function_exists('vtables_table_exists')) { 2050 function vtables_table_exists($table_id) { 2051 global $wpdb; 2052 $found = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2053 $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}vtables WHERE id = %d", $table_id ) 214 2054 ); 215 } 216 217 // Update source 218 $results = $wpdb->update( 219 $table_name, 220 array( 221 'title' => $title, 222 'file_size' => $file_size, 223 'source' => $sourceJSON, 224 'updated_at' => date('Y-m-d h:i:s'), 225 ), 226 array( 'id' => $data['id'] ), 227 array( '%s', '%d', '%s', '%s' ), 228 array( '%d' ) 229 ); 230 231 $query = $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id); 232 $updated_table = $wpdb->get_results($query)[0]; 233 $updated_table->source = json_decode($updated_table->source); 234 $response = $updated_table; 235 236 $rest_response = new WP_REST_Response( 237 array( 238 'success' => !!$results, 239 'data' => $response, 240 ) 241 ); 242 243 vtables_maybe_send_nocache_headers($rest_response); 244 245 return $rest_response; 246 } 247 248 249 /** 250 * SAVE TABLE SETTINGS. 251 */ 252 add_action( 'rest_api_init', function () { 253 register_rest_route( 'vtables/v1', '/settings', array( 254 'methods' => 'PUT', 255 'callback' => 'vtables_save_table_settings_handler', 256 'permission_callback' => 'vtables_admin_permission_callback', 257 ) ); 258 } ); 259 260 function vtables_save_table_settings_handler( WP_REST_Request $data ) { 261 global $wpdb; 262 263 $table_name = $wpdb->prefix . 'vtables'; 264 $settings = $data['settings']; 265 $settingsJSON = json_encode($data['settings']); 266 267 $results = $wpdb->update( 268 $table_name, 269 array( 270 'settings' => $settingsJSON, 271 'updated_at' => date('Y-m-d h:i:s'), 272 ), 273 array( 'id' => $data['id'] ), 274 array( '%s', '%s' ), 275 array( '%d' ) 276 ); 277 278 $response = array(); 279 $response['success'] = true; 280 $response['updated_at'] = date('Y-m-d h:i:s'); 281 282 $rest_response = new WP_REST_Response( 283 array( 284 'success' => !!$results, 285 'data' => $response, 286 ) 287 ); 288 289 vtables_maybe_send_nocache_headers($rest_response); 290 291 return $rest_response; 292 } 293 294 295 296 /** 297 * DELETE TABLE. 298 */ 299 add_action( 'rest_api_init', function () { 300 register_rest_route( 'vtables/v1', '/tables', array( 301 'methods' => 'DELETE', 302 'callback' => 'vtables_delete_record_handler', 303 'permission_callback' => 'vtables_admin_permission_callback', 304 ) ); 305 } ); 306 307 function vtables_delete_record_handler( WP_REST_Request $data ) { 308 global $wpdb; 309 $table_name = $wpdb->prefix . 'vtables'; 310 $results = $wpdb->delete( 311 $table_name, 312 array('id' => $data['id']), 313 ); 314 315 $response = array(); 316 $response['id'] = $data['id']; 317 318 $rest_response = new WP_REST_Response( 319 array( 320 'success' => !!$results, 321 'data' => $response, 322 ) 323 ); 324 325 vtables_maybe_send_nocache_headers($rest_response); 326 327 return $rest_response; 328 } 329 330 331 /** 332 * CUSTOM PERMISSION CALLBACK: accessible only by administrator. 333 */ 334 function vtables_admin_permission_callback() { 335 if ( 336 current_user_can('administrator') || 337 current_user_can('editor') || 338 current_user_can('shop_manager') 339 ) { 340 return true; 341 } 342 return new WP_Error( 343 'rest_forbidden', 344 'You do not have permissions to access this endpoint.', 345 array('status' => 401) 346 ); 347 } 348 349 350 /** 351 * Add no-cache headers for requests so aggressive caches 352 * (LiteSpeed, reverse proxies) don't serve stale data. 353 */ 354 function vtables_maybe_send_nocache_headers( WP_REST_Response $response ) { 355 $response->header( 'Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0' ); 356 $response->header( 'Pragma', 'no-cache' ); 357 $response->header( 'Expires', '0' ); 358 } 2055 return $found !== null; 2056 } 2057 } 2058 2059 /** 2060 * HELPER: per-table index map (column name -> array('type','slot')) read from 2061 * wp_vtables.source.columns. Only columns flagged indexed with a 1..5 slot are 2062 * returned. Drives the slot-backed sort/filter fast path in the query layer. 2063 */ 2064 if (!function_exists('vtables_table_index_map')) { 2065 function vtables_table_index_map($table_id) { 2066 global $wpdb; 2067 $source = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2068 $wpdb->prepare( "SELECT source FROM {$wpdb->prefix}vtables WHERE id = %d", $table_id ) 2069 ); 2070 $decoded = json_decode($source, true); 2071 $map = array(); 2072 if (is_array($decoded) && isset($decoded['columns']) && is_array($decoded['columns'])) { 2073 foreach ($decoded['columns'] as $col) { 2074 if ( 2075 isset($col['name'], $col['slot']) && 2076 !empty($col['indexed']) && 2077 (int) $col['slot'] >= 1 && (int) $col['slot'] <= 5 2078 ) { 2079 $type = isset($col['dataType']) ? (string) $col['dataType'] : 'text'; 2080 $map[(string) $col['name']] = array('type' => $type, 'slot' => (int) $col['slot']); 2081 } 2082 } 2083 } 2084 return $map; 2085 } 2086 } 2087 2088 /** 2089 * HELPER: type-sniff each column from a sample of rows. 2090 * Rules over the non-empty sample values: all-numeric -> number; all parseable 2091 * dates -> date; only boolean-ish (0/1/true/false/yes/no) -> boolean; else text. 2092 */ 2093 if (!function_exists('vtables_sniff_column_types')) { 2094 function vtables_sniff_column_types($headers, $sample_rows) { 2095 $types = array(); 2096 if (!is_array($headers)) { 2097 return $types; 2098 } 2099 $bool_set = array('0', '1', 'true', 'false', 'yes', 'no', 'y', 'n', 'on', 'off'); 2100 2101 foreach ($headers as $header) { 2102 $name = (string) $header; 2103 $seen = 0; 2104 $all_num = true; 2105 $all_date = true; 2106 $all_bool = true; 2107 2108 if (is_array($sample_rows)) { 2109 foreach ($sample_rows as $row) { 2110 if (!is_array($row) || !array_key_exists($name, $row)) { 2111 continue; 2112 } 2113 $val = $row[$name]; 2114 if (is_array($val) || is_object($val)) { 2115 $all_num = $all_date = $all_bool = false; 2116 $seen++; 2117 continue; 2118 } 2119 $str = trim((string) $val); 2120 if ($str === '') { 2121 continue; // empties don't constrain the type 2122 } 2123 $seen++; 2124 if (!is_numeric($str)) { 2125 $all_num = false; 2126 } 2127 if (!in_array(strtolower($str), $bool_set, true)) { 2128 $all_bool = false; 2129 } 2130 // A bare integer is ambiguous as a date; require a non-numeric parseable 2131 // string for the date guess so years/ids don't all become dates. 2132 if (is_numeric($str) || strtotime($str) === false) { 2133 $all_date = false; 2134 } 2135 } 2136 } 2137 2138 if ($seen === 0) { 2139 $types[$name] = 'text'; 2140 } elseif ($all_bool) { 2141 $types[$name] = 'boolean'; 2142 } elseif ($all_num) { 2143 $types[$name] = 'number'; 2144 } elseif ($all_date) { 2145 $types[$name] = 'date'; 2146 } else { 2147 $types[$name] = 'text'; 2148 } 2149 } 2150 2151 return $types; 2152 } 2153 } 2154 2155 /** 2156 * HELPER: count-cache key for a table + a query signature (the key scheme is 2157 * shared with vTables Pro, which also folds search/filter into it). 2158 */ 2159 if (!function_exists('vtables_count_cache_key')) { 2160 function vtables_count_cache_key($table_id, $signature) { 2161 return 'vtables_count_' . (int) $table_id . '_' . md5((string) $signature); 2162 } 2163 } 2164 2165 /** 2166 * HELPER: invalidate every cached COUNT for a table. Bumps a per-table version 2167 * stored in an option; cache keys fold the version in, so a single bump expires 2168 * all of a table's count transients without enumerating them. The option and 2169 * transient names are the ones vTables Pro uses, so an edit made by either 2170 * plugin expires both plugins' cached counts. 2171 */ 2172 if (!function_exists('vtables_count_cache_version')) { 2173 function vtables_count_cache_version($table_id) { 2174 $v = get_option('vtables_count_ver_' . (int) $table_id); 2175 return $v ? (int) $v : 1; 2176 } 2177 } 2178 if (!function_exists('vtables_invalidate_count_cache')) { 2179 function vtables_invalidate_count_cache($table_id) { 2180 $key = 'vtables_count_ver_' . (int) $table_id; 2181 update_option($key, vtables_count_cache_version($table_id) + 1, false); 2182 } 2183 } 2184 2185 /** 2186 * HELPER: paginated read over wp_vtables_source, in row order. 2187 * Returns array('rows' => array<assoc row + vt_id>, 'total' => int). 2188 * Search, sorting and filtering are vTables Pro features; this is the plain 2189 * pagination path of Pro's query layer (same row order, same keyset cursor, 2190 * same cached count), so page boundaries match whichever plugin serves them. 2191 * 2192 * @param array $args table_id, page, per_page, after_row_index 2193 */ 2194 if (!function_exists('vtables_query_source_rows')) { 2195 function vtables_query_source_rows($args) { 2196 global $wpdb; 2197 2198 $table_id = isset($args['table_id']) ? (int) $args['table_id'] : 0; 2199 $page = isset($args['page']) ? max(1, (int) $args['page']) : 1; 2200 $per_page = isset($args['per_page']) ? (int) $args['per_page'] : vtables_default_per_page(); 2201 $per_page = max(1, min(1000, $per_page)); 2202 // Keyset cursor for flat-cost next/prev. 2203 $after_row_index = isset($args['after_row_index']) && $args['after_row_index'] !== '' ? (int) $args['after_row_index'] : null; 2204 2205 // COUNT(*) caching: the total only changes on insert/delete, so cache it 2206 // per table and invalidation version. 2207 $cache_key = vtables_count_cache_key($table_id, vtables_count_cache_version($table_id) . '|all'); 2208 $total = get_transient($cache_key); 2209 if ($total === false) { 2210 $total = (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2211 $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $table_id ) 2212 ); 2213 set_transient($cache_key, $total, HOUR_IN_SECONDS); 2214 } else { 2215 $total = (int) $total; 2216 } 2217 2218 if ($after_row_index !== null) { 2219 // Uses the (table_id, row_index) index for flat cost at any depth. 2220 $records = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2221 $wpdb->prepare( "SELECT id, row_index, row_json FROM {$wpdb->prefix}vtables_source WHERE table_id = %d AND row_index > %d ORDER BY row_index ASC, id ASC LIMIT %d", $table_id, $after_row_index, $per_page ), 2222 ARRAY_A 2223 ); 2224 } else { 2225 $records = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2226 $wpdb->prepare( "SELECT id, row_index, row_json FROM {$wpdb->prefix}vtables_source WHERE table_id = %d ORDER BY row_index ASC, id ASC LIMIT %d OFFSET %d", $table_id, $per_page, ($page - 1) * $per_page ), 2227 ARRAY_A 2228 ); 2229 } 2230 2231 $rows = array(); 2232 if (is_array($records)) { 2233 foreach ($records as $record) { 2234 $decoded = json_decode($record['row_json'], true); 2235 if (!is_array($decoded)) { 2236 $decoded = array(); 2237 } 2238 $decoded['vt_id'] = (int) $record['id']; 2239 $decoded['vt_row_index'] = (int) $record['row_index']; 2240 $rows[] = $decoded; 2241 } 2242 } 2243 2244 return array('rows' => $rows, 'total' => $total); 2245 } 2246 } 2247 2248 2249 /** 2250 * HELPER: build the lowercased free-text search blob for one row. 2251 * Concatenates all cell values so a single LIKE can match across columns. 2252 */ 2253 if (!function_exists('vtables_row_search_blob')) { 2254 function vtables_row_search_blob($row) { 2255 if (!is_array($row)) { 2256 $row = (array) $row; 2257 } 2258 $parts = array(); 2259 foreach ($row as $value) { 2260 if (is_scalar($value)) { 2261 $parts[] = (string) $value; 2262 } elseif ($value !== null) { 2263 $parts[] = wp_json_encode($value); 2264 } 2265 } 2266 return strtolower(implode(' ', $parts)); 2267 } 2268 } 2269 2270 2271 /** 2272 * HELPER: replace all source rows for a table. 2273 * Deletes existing wp_vtables_source records for $table_id and re-inserts $rows as 2274 * one record per row, batched so each INSERT packet stays small (well under 2275 * max_allowed_packet). row_index is contiguous starting at 0. 2276 */ 2277 if (!function_exists('vtables_replace_source_rows')) { 2278 function vtables_replace_source_rows($table_id, $rows) { 2279 global $wpdb; 2280 $source_table = $wpdb->prefix . 'vtables_source'; 2281 2282 $wpdb->delete($source_table, array('table_id' => $table_id), array('%d')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery 2283 vtables_invalidate_count_cache($table_id); 2284 2285 if (empty($rows) || !is_array($rows)) { 2286 return; 2287 } 2288 2289 // Populate the typed slot sub-columns from the table's index map (if any). 2290 $index_map = vtables_table_index_map($table_id); 2291 $slot_cols = vtables_slot_columns(); 2292 $col_list = array_merge(array('table_id', 'row_index', 'row_json', 'search_blob'), $slot_cols); 2293 2294 $batch_size = 500; 2295 $index = 0; 2296 $values = array(); 2297 $params = array(); 2298 2299 $flush = function () use (&$values, &$params, $source_table, $col_list, $wpdb) { 2300 if (empty($values)) { 2301 return; 2302 } 2303 $sql = "INSERT INTO {$source_table} (" . implode(', ', $col_list) . ") VALUES " . implode(', ', $values); 2304 $wpdb->query( $wpdb->prepare($sql, $params) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql holds only fixed column names and placeholders; every value goes through prepare() 2305 $values = array(); 2306 $params = array(); 2307 }; 2308 2309 foreach (array_values($rows) as $row) { 2310 $row_json = wp_json_encode($row); 2311 if ($row_json === false) { 2312 continue; 2313 } 2314 $slot_values = vtables_row_index_values($row, $index_map); 2315 $placeholders = array('%d', '%d', '%s', '%s'); 2316 $params[] = $table_id; 2317 $params[] = $index; 2318 $params[] = $row_json; 2319 $params[] = vtables_row_search_blob($row); 2320 foreach ($slot_cols as $slot_col) { 2321 if (!isset($slot_values[$slot_col]) || $slot_values[$slot_col] === null) { 2322 $placeholders[] = 'NULL'; 2323 } else { 2324 $placeholders[] = '%s'; 2325 $params[] = $slot_values[$slot_col]; 2326 } 2327 } 2328 $values[] = '(' . implode(', ', $placeholders) . ')'; 2329 $index++; 2330 2331 if (count($values) >= $batch_size) { 2332 $flush(); 2333 } 2334 } 2335 2336 $flush(); 2337 } 2338 } 2339 2340 2341 /** 2342 * HELPER: private working directory for uploads in progress. 2343 * uploads/vtables_uploads/ holds chunks and assembled files only until the 2344 * import has read them. Created on demand with an index.php and a deny-all 2345 * .htaccess, so partial uploads are never browsable or directly fetchable. 2346 * Returns the path with a trailing slash, or '' when it can't be created. 2347 */ 2348 if (!function_exists('vtables_uploads_dir')) { 2349 function vtables_uploads_dir() { 2350 $upload_info = wp_upload_dir(); 2351 $dir = trailingslashit($upload_info['basedir']) . 'vtables_uploads/'; 2352 2353 if (!wp_mkdir_p($dir)) { 2354 return ''; 2355 } 2356 2357 if (!file_exists($dir . 'index.php')) { 2358 file_put_contents($dir . 'index.php', "<?php\n// Silence is golden.\n"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents 2359 } 2360 2361 if (!file_exists($dir . '.htaccess')) { 2362 file_put_contents($dir . '.htaccess', "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents 2363 } 2364 2365 return $dir; 2366 } 2367 } 2368 2369 2370 /** 2371 * HELPER: store an uploaded file (a $_FILES entry) as $dir . $filename. 2372 * Goes through wp_handle_upload(), which runs the is_uploaded_file() check 2373 * and the upload filters, pointed at $dir instead of the media library 2374 * folders. Its type test is off: callers allowlist the extension themselves, 2375 * and chunks are raw byte ranges. Returns the stored path, or '' on failure. 2376 */ 2377 if (!function_exists('vtables_store_uploaded_file')) { 2378 function vtables_store_uploaded_file($file, $dir, $filename) { 2379 require_once ABSPATH . 'wp-admin/includes/file.php'; 2380 2381 $use_dir = function ($uploads) use ($dir) { 2382 $uploads['path'] = untrailingslashit($dir); 2383 return $uploads; 2384 }; 2385 2386 add_filter('upload_dir', $use_dir); 2387 $result = wp_handle_upload($file, array( 2388 'test_form' => false, 2389 'test_type' => false, 2390 'unique_filename_callback' => function () use ($filename) { 2391 return $filename; 2392 }, 2393 )); 2394 remove_filter('upload_dir', $use_dir); 2395 2396 return empty($result['file']) ? '' : $result['file']; 2397 } 2398 } 2399 2400 2401 /** 2402 * HELPER: recursively remove a directory. 2403 */ 2404 if (!function_exists('vtables_remove_directory')) { 2405 function vtables_remove_directory($dir) { 2406 if (!is_dir($dir)) { 2407 return; 2408 } 2409 $files = array_diff(scandir($dir), array('.', '..')); 2410 foreach ($files as $file) { 2411 $path = $dir . $file; 2412 if (is_dir($path)) { 2413 vtables_remove_directory($path . '/'); 2414 } else { 2415 wp_delete_file($path); 2416 } 2417 } 2418 rmdir($dir); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir 2419 } 2420 } -
vtables/trunk/includes/shortcode.html
r3133736 r3754379 1 1 <!DOCTYPE html> 2 <html lang="en-US"> 3 <head> 4 <meta charset="utf-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>vTables</title> 7 <link 8 href="../dist/spa/css/material-icons-font-dist.css" 9 rel="stylesheet" 10 type="text/css" 11 /> 12 <link 13 href="../dist/spa/css/quasar.prod.css" 14 rel="stylesheet" 15 type="text/css" 16 /> 17 <link href="../dist/spa/css/vtables.css" rel="stylesheet" type="text/css" /> 18 </head> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="robots" content="noindex, nofollow"> 6 <title>vTables</title> 7 </head> 8 <body> 9 <script> 10 /* 11 * Compatibility stub for pages cached while vTables 0.2.x was active: its 12 * [vtables] iframe pointed here and carried the table id and the REST base 13 * URL as data attributes. Send such an iframe on to the current embed 14 * document (?vtables_embed=<id>, which works with and without pretty 15 * permalinks). Pages rendered by vTables 1.0 never load this file. 16 */ 17 (function () { 18 try { 19 var frame = window.frameElement; 20 if (!frame) return; 19 21 20 <body> 21 <div class="vtable q-pa-sm"> 22 <q-table 23 v-if="selectedTable" 24 v-model:pagination="settings.bottom.initial" 25 :class="tableCssClasses" 26 :columns="columns" 27 :rows="rows" 28 :grid="settings.general.grid" 29 :bordered="settings.general.bordered" 30 :table-style="tableStyle" 31 :card-class="cardClasses" 32 :card-style="cardStyle" 33 :dark="settings.general.dark" 34 :dense="settings.general.dense" 35 :flat="settings.general.flat || !selectedTable.source.rows.length" 36 :square="settings.general.square" 37 :separator="settings.general.separator" 38 > 39 <!-- TOP --> 40 <template #top="props"> 41 <div class="vtable__top | full-width flex" :class="topClasses"> 42 <div v-if="!settings.top.hideTitle" class="col"> 43 <h1 44 class="q-mt-xs q-mr-xs q-mb-xs q-ml-none text-h5" 45 :class=" 46 settings.top.textColor 47 ? `text-${settings.top.textColor}` 48 : `text-${settings.general.textColor}` 49 " 50 > 51 {{ selectedTable.title }} 52 </h1> 53 </div> 54 </div> 55 </template> 22 var id = parseInt(frame.getAttribute('data-id'), 10); 23 var base = frame.getAttribute('data-baseurl') || ''; 24 if (!id || id < 1 || !/^https?:\/\//i.test(base)) return; 56 25 57 <!-- BODY --> 58 <template #body="props"> 59 <q-tr :props="props"> 60 <q-td v-for="col in props.cols" :key="col.name" :props="props"> 61 {{ props.row[col.field] || props.row[col.label] }} 62 </q-td> 63 </q-tr> 64 </template> 26 // rest_url() is either .../wp-json/ or .../?rest_route=/ (plain permalinks). 27 var home = base.replace(/\?rest_route=.*$/, '').replace(/wp-json\/?$/, ''); 28 var separator = home.indexOf('?') === -1 ? '?' : '&'; 65 29 66 <!-- GRID --> 67 <template v-if="settings.general.grid" #item="props"> 68 <div 69 class="vtable__grid | q-pa-xs col-xs-12 col-sm-6 col-md-4 grid-style-transition" 70 :class="gridColumnWidth" 71 > 72 <q-card 73 class="q-pt-sm full-height" 74 :bordered="props.bordered" 75 :dark="props.dark" 76 :dense="props.dense" 77 :flat="props.flat" 78 :square="props.square" 79 :class="[ 80 `bg-${settingsGeneralBackgroundColor}`, 81 props.cardClass, 82 ]" 83 > 84 <q-list> 85 <q-item v-for="col in props.cols" :key="col.name"> 86 <q-item-section v-if="col.value"> 87 <q-item-label 88 caption 89 :class="`text-${settingsGeneralTextColor}`" 90 > 91 {{ col.label }} 92 </q-item-label> 93 <q-item-label 94 :class="[ 95 `text-${settingsGeneralTextColor}`, 96 ]" 97 > 98 <div 99 v-if="renderLinksAsLinks(col.value)" 100 class="flex gap-5" 101 > 102 <template 103 v-for="link in col.value.split(' ')" 104 :key="link" 105 > 106 <a 107 :href="link" 108 target="_blank" 109 :class="`text-${settings.content.buttonTextColor}`" 110 > 111 <span v-if="settings.content.buttonText"> 112 {{ settings.content.buttonText }} 113 </span> 114 <span v-else> Link </span> 115 </a> 116 </template> 117 </div> 118 <div v-else class="cell-value">{{ col.value }}</div> 119 </q-item-label> 120 </q-item-section> 121 </q-item> 122 </q-list> 123 </q-card> 124 </div> 125 </template> 126 </q-table> 127 128 <!-- Show empty table while loading --> 129 <q-table 130 v-else-if="loading" 131 :loading="loading" 132 :rows="[]" 133 :columns="[]" 134 ></q-table> 135 136 <!-- Show empty table if no data --> 137 <q-table 138 v-else 139 :rows="[]" 140 :columns="[]" 141 :no-data-label="noDataLabel" 142 ></q-table> 143 </div> 144 145 <!-- Add the following at the end of your body tag --> 146 <script src="../dist/spa/js/vue.global.prod.js"></script> 147 <script src="../dist/spa/js/quasar.umd.prod.js"></script> 148 149 <script> 150 const iframe = window.frameElement; 151 const dataId = iframe.getAttribute('data-id'); 152 const dataCount = iframe.getAttribute('data-count'); 153 const baseUrl = iframe.getAttribute('data-baseurl'); 154 const table = document.querySelector('.vtable'); 155 156 if (table) { 157 // Set attribute 158 table.setAttribute('id', 'vtable-' + dataCount); 159 // Add class 160 table.classList.add('vtable-' + dataId); 161 } 162 163 // Vue instance 164 const app = Vue.createApp({ 165 data() { 166 return { 167 dataCount: null, 168 dataId: null, 169 selectedTable: null, 170 loading: false, 171 title: '', 172 columns: [], 173 rows: [], 174 settings: {}, 175 initialPagination: {}, 176 settingsSearchFilter: '', 177 showFullImage: false, 178 imagePath: '', 179 tableMaximized: false, 180 }; 181 }, 182 async mounted() { 183 this.loading = true; 184 this.dataCount = dataCount; 185 this.dataId = dataId; 186 187 // Fetch data 188 try { 189 this.selectedTable = await this.getTable(dataId); 190 } catch (error) { 191 console.log(error); 192 } 193 194 this.loading = false; 195 196 // Set height of iframe if no data 197 if (!this.selectedTable) { 198 iframe.style.height = '70px'; 199 return; 200 } 201 202 // Descruct data 203 const { source, settings, title } = this.selectedTable; 204 205 // Assign data 206 this.columns = source.columns; 207 this.rows = source.rows; 208 this.settings = settings; 209 this.initialPagination = settings.bottom.initial; 210 this.title = title; 211 212 // Dynamic Height 213 this.$nextTick(() => { 214 const bodyHeight = document.body.clientHeight; 215 iframe.style.minHeight = bodyHeight + 20 + 'px'; 216 }); 217 218 // Dynamic width 219 if (this.settings.dimensions.tableMaxWidth) { 220 iframe.style.maxWidth = 221 this.settings.dimensions.tableMaxWidth + 'px'; 222 } 223 224 // Add event listener 225 window.addEventListener('resize', this.handleResize); 226 }, 227 beforeDestroy() { 228 // Destroy event listener 229 window.removeEventListener('resize', this.handleResize); 230 }, 231 computed: { 232 // Table style computed prop. 233 tableStyle() { 234 if (Object.keys(this.settings).length) { 235 return { 236 height: this.settings.dimensions.tableHeight 237 ? this.settings.dimensions.tableHeight + 'px' 238 : '100%', 239 }; 240 } 241 242 return ''; 243 }, 244 245 // No data label computed prop. 246 noDataLabel() { 247 if (!this.selectedTable) { 248 return 'Table with this shortcode does not exist'; 249 } 250 251 return 'No data available'; 252 }, 253 254 // Card classes computed prop. 255 cardClasses() { 256 if (Object.keys(this.settings).length) { 257 const cssClasses = [this.settings.customCss.cardClass]; 258 259 if (this.settings.general.backgroundColor) { 260 cssClasses.push(`bg-${this.settings.general.backgroundColor}`); 261 } 262 263 if (this.settings.general.textColor) { 264 cssClasses.push(`text-${this.settings.general.textColor}`); 265 } 266 267 return cssClasses; 268 } 269 270 return []; 271 }, 272 273 // Card style computed prop. 274 cardStyle() { 275 if (Object.keys(this.settings).length) { 276 return { 277 maxWidth: this.settings.general.tableMaxWidth 278 ? this.settings.general.tableMaxWidth + 'px' 279 : '100%', 280 }; 281 } 282 283 return ''; 284 }, 285 286 // Top classes computed prop. 287 topClasses() { 288 if (Object.keys(this.settings).length) { 289 const cssClasses = []; 290 291 if (this.settings.top.topBackgroundColor) { 292 cssClasses.push(`bg-${this.settings.top.topBackgroundColor}`); 293 } 294 295 if (this.settings.top.textColor) { 296 cssClasses.push(`text-${this.settings.top.textColor}`); 297 } 298 299 return cssClasses; 300 } 301 302 return []; 303 }, 304 305 // Table CSS classes computed prop. 306 tableCssClasses() { 307 const isAdminbarPresented = document.querySelector('#wpadminbar'); 308 return [ 309 { 'sticky-header-table': this.settings.top.stickyHeader }, 310 { 311 'sticky-column-first': this.settings.columns.stickyFirstColumn, 312 }, 313 { 'sticky-column-last': this.settings.columns.stickyLastColumn }, 314 { 315 'admin-bar-presented-on-fullscreen': 316 this.tableMaximized && isAdminbarPresented, 317 }, 318 ]; 319 }, 320 321 // Grid column width computed prop. 322 gridColumnWidth() { 323 if ( 324 this.settings.bottom.initial && 325 this.settings.bottom.initial.rowsPerPage 326 ) { 327 return `col-sm-${12 / this.settings.bottom.initial.rowsPerPage}`; 328 } 329 return 'col-sm-3'; 330 }, 331 332 // General text color computed prop. 333 settingsGeneralTextColor() { 334 if (Object.keys(this.settings).length) { 335 return this.settings.general.textColor; 336 } 337 return ''; 338 }, 339 340 // General background color computed prop. 341 settingsGeneralBackgroundColor() { 342 if (Object.keys(this.settings).length) { 343 return this.settings.general.backgroundColor; 344 } 345 return ''; 346 }, 347 }, 348 setup() { 349 /** 350 * FETCH TABLE 351 */ 352 async function getTable(id) { 353 const base = baseUrl || '/wp-json/'; 354 const post = await fetch(`${base}vtables/v1/tables/${id}`).then( 355 (resp) => resp.json() 356 ); 357 358 if (!post || post?.data?.status === 404) { 359 return; 360 } 361 362 return post.data; 363 } 364 365 /** 366 * CHECK IF LINK 367 * @param str 368 */ 369 function getLinkType(str) { 370 const regex = /http|https|jpg|jpeg|png|gif|bmp|tiff|webp|svg/i; 371 372 if (!regex.test(str)) { 373 return; 374 } 375 376 const linkPattern = /^(https?:\/\/\S+)$/i; 377 const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|tiff|webp|svg)$/i; 378 const strArr = str.split(' '); 379 const isLinks = strArr.every((el) => linkPattern.test(el)); 380 const isImages = strArr.every((el) => imageExtensions.test(el)); 381 382 if (isImages) { 383 return 'images'; 384 } 385 386 if (isLinks) { 387 return 'links'; 388 } 389 } 390 391 /** 392 * RENDER LINKS AS LINKS 393 * @param value 394 */ 395 function renderLinksAsLinks(value) { 396 if (!Object.keys(this.settings).length) { 397 return; 398 } 399 400 return ( 401 (!this.settings.content.turnLinksToImages && 402 getLinkType(value) === 'images') || 403 getLinkType(value) === 'links' 404 ); 405 } 406 407 /** 408 * HANDLE RESIZE 409 */ 410 function handleResize() { 411 const bodyHeight = document.body.clientHeight; 412 iframe.style.minHeight = bodyHeight + 20 + 'px'; 413 } 414 415 return { 416 getTable, 417 renderLinksAsLinks, 418 getLinkType, 419 handleResize, 420 }; 421 }, 422 }); 423 424 app.use(Quasar); 425 app.mount('#vtable-' + dataCount); 426 </script> 427 </body> 30 window.location.replace(home + separator + 'vtables_embed=' + id); 31 } catch (e) { 32 // Cross-origin parent: nothing to read; the frame stays blank. 33 } 34 })(); 35 </script> 36 </body> 428 37 </html> -
vtables/trunk/index.html
r3134061 r3754379 12 12 content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>" 13 13 /> 14 <link rel="stylesheet" href="./public/css/material-icons-font-dist.css" /> 14 15 </head> 15 16 <body> -
vtables/trunk/package-lock.json
r3036822 r3754379 1 1 { 2 2 "name": "vtables", 3 "version": " 0.0.1",3 "version": "3.0.0", 4 4 "lockfileVersion": 3, 5 5 "requires": true, … … 7 7 "": { 8 8 "name": "vtables", 9 "version": "0.0.1", 9 "version": "3.0.0", 10 "license": "GPL-3.0-only", 10 11 "dependencies": { 11 12 "@quasar/extras": "^1.0.0", 12 13 "@types/papaparse": "^5.3.9", 13 "@types/wpapi": "^1.1.1",14 14 "@vuelidate/core": "^2.0.3", 15 15 "@vuelidate/validators": "^2.0.3", … … 22 22 "vue": "^3.0.0", 23 23 "vue-i18n": "^9.2.2", 24 "vue-router": "^4.0.0", 25 "wpapi": "^1.2.2" 24 "vue-router": "^4.0.0" 26 25 }, 27 26 "devDependencies": { … … 37 36 "eslint-plugin-vue": "^9.0.0", 38 37 "prettier": "^2.5.1", 39 "typescript": "5.0.4" 38 "typescript": "5.0.4", 39 "vue-tsc": "^1.8.27" 40 40 }, 41 41 "engines": { … … 694 694 "license": "MIT" 695 695 }, 696 "node_modules/@types/wpapi": {697 "version": "1.1.1",698 "resolved": "https://registry.npmjs.org/@types/wpapi/-/wpapi-1.1.1.tgz",699 "integrity": "sha512-Ek1siuveAaN/iVoRvGbSGWNBg7RXQoz4Am1C3nnoXVA33f/0+9GibJq/xNBWsInZk5mNVqySkeYVco5C1KMqog==",700 "license": "MIT"701 },702 696 "node_modules/@typescript-eslint/eslint-plugin": { 703 697 "version": "5.59.11", … … 934 928 } 935 929 }, 930 "node_modules/@volar/language-core": { 931 "version": "1.11.1", 932 "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", 933 "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", 934 "dev": true, 935 "dependencies": { 936 "@volar/source-map": "1.11.1" 937 } 938 }, 939 "node_modules/@volar/source-map": { 940 "version": "1.11.1", 941 "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", 942 "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", 943 "dev": true, 944 "dependencies": { 945 "muggle-string": "^0.3.1" 946 } 947 }, 948 "node_modules/@volar/typescript": { 949 "version": "1.11.1", 950 "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", 951 "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", 952 "dev": true, 953 "dependencies": { 954 "@volar/language-core": "1.11.1", 955 "path-browserify": "^1.0.1" 956 } 957 }, 936 958 "node_modules/@vue/compiler-core": { 937 959 "version": "3.3.4", … … 990 1012 "license": "MIT" 991 1013 }, 1014 "node_modules/@vue/language-core": { 1015 "version": "1.8.27", 1016 "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", 1017 "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", 1018 "dev": true, 1019 "dependencies": { 1020 "@volar/language-core": "~1.11.1", 1021 "@volar/source-map": "~1.11.1", 1022 "@vue/compiler-dom": "^3.3.0", 1023 "@vue/shared": "^3.3.0", 1024 "computeds": "^0.0.1", 1025 "minimatch": "^9.0.3", 1026 "muggle-string": "^0.3.1", 1027 "path-browserify": "^1.0.1", 1028 "vue-template-compiler": "^2.7.14" 1029 }, 1030 "peerDependencies": { 1031 "typescript": "*" 1032 }, 1033 "peerDependenciesMeta": { 1034 "typescript": { 1035 "optional": true 1036 } 1037 } 1038 }, 1039 "node_modules/@vue/language-core/node_modules/brace-expansion": { 1040 "version": "2.1.7", 1041 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", 1042 "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", 1043 "dev": true, 1044 "dependencies": { 1045 "balanced-match": "^1.0.0" 1046 } 1047 }, 1048 "node_modules/@vue/language-core/node_modules/minimatch": { 1049 "version": "9.0.9", 1050 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", 1051 "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", 1052 "dev": true, 1053 "dependencies": { 1054 "brace-expansion": "^2.0.2" 1055 }, 1056 "engines": { 1057 "node": ">=16 || 14 >=14.17" 1058 }, 1059 "funding": { 1060 "url": "https://github.com/sponsors/isaacs" 1061 } 1062 }, 992 1063 "node_modules/@vue/reactivity": { 993 1064 "version": "3.3.4", … … 1223 1294 "engines": { 1224 1295 "node": ">=0.4.0" 1296 } 1297 }, 1298 "node_modules/agent-base": { 1299 "version": "6.0.2", 1300 "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 1301 "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 1302 "dependencies": { 1303 "debug": "4" 1304 }, 1305 "engines": { 1306 "node": ">= 6.0.0" 1225 1307 } 1226 1308 }, … … 1467 1549 }, 1468 1550 "node_modules/axios": { 1469 "version": "1. 4.0",1470 "resolved": "https://registry.npmjs.org/axios/-/axios-1. 4.0.tgz",1471 "integrity": "sha512- S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",1472 " license": "MIT",1473 "dependencies": {1474 "fo llow-redirects": "^1.15.0",1475 " form-data": "^4.0.0",1476 "proxy-from-env": "^ 1.1.0"1551 "version": "1.18.1", 1552 "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", 1553 "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", 1554 "dependencies": { 1555 "follow-redirects": "^1.16.0", 1556 "form-data": "^4.0.5", 1557 "https-proxy-agent": "^5.0.1", 1558 "proxy-from-env": "^2.1.0" 1477 1559 } 1478 1560 }, … … 1682 1764 "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 1683 1765 "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 1766 "dev": true, 1684 1767 "license": "MIT", 1685 1768 "dependencies": { … … 1689 1772 "funding": { 1690 1773 "url": "https://github.com/sponsors/ljharb" 1774 } 1775 }, 1776 "node_modules/call-bind-apply-helpers": { 1777 "version": "1.0.2", 1778 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 1779 "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 1780 "dependencies": { 1781 "es-errors": "^1.3.0", 1782 "function-bind": "^1.1.2" 1783 }, 1784 "engines": { 1785 "node": ">= 0.4" 1691 1786 } 1692 1787 }, … … 1929 2024 "license": "MIT" 1930 2025 }, 1931 "node_modules/component-emitter": {1932 "version": "1.3.0",1933 "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",1934 "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",1935 "license": "MIT"1936 },1937 2026 "node_modules/compress-commons": { 1938 2027 "version": "4.1.1", … … 2017 2106 "license": "MIT" 2018 2107 }, 2108 "node_modules/computeds": { 2109 "version": "0.0.1", 2110 "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", 2111 "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", 2112 "dev": true 2113 }, 2019 2114 "node_modules/concat-map": { 2020 2115 "version": "0.0.1", … … 2064 2159 "license": "MIT" 2065 2160 }, 2066 "node_modules/cookiejar": {2067 "version": "2.1.4",2068 "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",2069 "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",2070 "license": "MIT"2071 },2072 2161 "node_modules/core-util-is": { 2073 2162 "version": "1.0.3", … … 2138 2227 "license": "MIT" 2139 2228 }, 2229 "node_modules/de-indent": { 2230 "version": "1.0.2", 2231 "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", 2232 "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", 2233 "dev": true 2234 }, 2140 2235 "node_modules/debug": { 2141 2236 "version": "4.3.4", … … 2257 2352 } 2258 2353 }, 2354 "node_modules/dunder-proto": { 2355 "version": "1.0.1", 2356 "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 2357 "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 2358 "dependencies": { 2359 "call-bind-apply-helpers": "^1.0.1", 2360 "es-errors": "^1.3.0", 2361 "gopd": "^1.2.0" 2362 }, 2363 "engines": { 2364 "node": ">= 0.4" 2365 } 2366 }, 2259 2367 "node_modules/ee-first": { 2260 2368 "version": "1.1.1", … … 2309 2417 "dependencies": { 2310 2418 "once": "^1.4.0" 2419 } 2420 }, 2421 "node_modules/es-define-property": { 2422 "version": "1.0.1", 2423 "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 2424 "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 2425 "engines": { 2426 "node": ">= 0.4" 2427 } 2428 }, 2429 "node_modules/es-errors": { 2430 "version": "1.3.0", 2431 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 2432 "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 2433 "engines": { 2434 "node": ">= 0.4" 2435 } 2436 }, 2437 "node_modules/es-object-atoms": { 2438 "version": "1.1.2", 2439 "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", 2440 "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", 2441 "dependencies": { 2442 "es-errors": "^1.3.0" 2443 }, 2444 "engines": { 2445 "node": ">= 0.4" 2446 } 2447 }, 2448 "node_modules/es-set-tostringtag": { 2449 "version": "2.1.0", 2450 "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 2451 "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 2452 "dependencies": { 2453 "es-errors": "^1.3.0", 2454 "get-intrinsic": "^1.2.6", 2455 "has-tostringtag": "^1.0.2", 2456 "hasown": "^2.0.2" 2457 }, 2458 "engines": { 2459 "node": ">= 0.4" 2311 2460 } 2312 2461 }, … … 2898 3047 }, 2899 3048 "node_modules/follow-redirects": { 2900 "version": "1.1 5.2",2901 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.1 5.2.tgz",2902 "integrity": "sha512- VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",3049 "version": "1.16.0", 3050 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", 3051 "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", 2903 3052 "funding": [ 2904 3053 { … … 2907 3056 } 2908 3057 ], 2909 "license": "MIT",2910 3058 "engines": { 2911 3059 "node": ">=4.0" … … 2918 3066 }, 2919 3067 "node_modules/form-data": { 2920 "version": "4.0.0", 2921 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 2922 "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 2923 "license": "MIT", 3068 "version": "4.0.6", 3069 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", 3070 "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", 2924 3071 "dependencies": { 2925 3072 "asynckit": "^0.4.0", 2926 3073 "combined-stream": "^1.0.8", 2927 "mime-types": "^2.1.12" 3074 "es-set-tostringtag": "^2.1.0", 3075 "hasown": "^2.0.4", 3076 "mime-types": "^2.1.35" 2928 3077 }, 2929 3078 "engines": { 2930 3079 "node": ">= 6" 2931 }2932 },2933 "node_modules/formidable": {2934 "version": "1.2.6",2935 "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",2936 "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",2937 "license": "MIT",2938 "funding": {2939 "url": "https://ko-fi.com/tunnckoCore/commissions"2940 3080 } 2941 3081 }, … … 3018 3158 }, 3019 3159 "node_modules/function-bind": { 3020 "version": "1.1.1", 3021 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 3022 "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 3023 "license": "MIT" 3160 "version": "1.1.2", 3161 "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 3162 "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 3163 "funding": { 3164 "url": "https://github.com/sponsors/ljharb" 3165 } 3024 3166 }, 3025 3167 "node_modules/get-caller-file": { … … 3034 3176 }, 3035 3177 "node_modules/get-intrinsic": { 3036 "version": "1.2.1", 3037 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 3038 "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 3039 "license": "MIT", 3040 "dependencies": { 3041 "function-bind": "^1.1.1", 3042 "has": "^1.0.3", 3043 "has-proto": "^1.0.1", 3044 "has-symbols": "^1.0.3" 3178 "version": "1.3.0", 3179 "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 3180 "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 3181 "dependencies": { 3182 "call-bind-apply-helpers": "^1.0.2", 3183 "es-define-property": "^1.0.1", 3184 "es-errors": "^1.3.0", 3185 "es-object-atoms": "^1.1.1", 3186 "function-bind": "^1.1.2", 3187 "get-proto": "^1.0.1", 3188 "gopd": "^1.2.0", 3189 "has-symbols": "^1.1.0", 3190 "hasown": "^2.0.2", 3191 "math-intrinsics": "^1.1.0" 3192 }, 3193 "engines": { 3194 "node": ">= 0.4" 3045 3195 }, 3046 3196 "funding": { 3047 3197 "url": "https://github.com/sponsors/ljharb" 3198 } 3199 }, 3200 "node_modules/get-proto": { 3201 "version": "1.0.1", 3202 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 3203 "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 3204 "dependencies": { 3205 "dunder-proto": "^1.0.1", 3206 "es-object-atoms": "^1.0.0" 3207 }, 3208 "engines": { 3209 "node": ">= 0.4" 3048 3210 } 3049 3211 }, … … 3119 3281 } 3120 3282 }, 3283 "node_modules/gopd": { 3284 "version": "1.2.0", 3285 "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 3286 "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 3287 "engines": { 3288 "node": ">= 0.4" 3289 }, 3290 "funding": { 3291 "url": "https://github.com/sponsors/ljharb" 3292 } 3293 }, 3121 3294 "node_modules/graceful-fs": { 3122 3295 "version": "4.2.11", … … 3144 3317 "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 3145 3318 "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 3319 "dev": true, 3146 3320 "license": "MIT", 3147 3321 "dependencies": { … … 3162 3336 } 3163 3337 }, 3164 "node_modules/has-proto": { 3165 "version": "1.0.1", 3166 "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 3167 "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 3168 "license": "MIT", 3338 "node_modules/has-symbols": { 3339 "version": "1.1.0", 3340 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 3341 "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 3169 3342 "engines": { 3170 3343 "node": ">= 0.4" … … 3174 3347 } 3175 3348 }, 3176 "node_modules/has-symbols": { 3177 "version": "1.0.3", 3178 "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 3179 "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 3180 "license": "MIT", 3349 "node_modules/has-tostringtag": { 3350 "version": "1.0.2", 3351 "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 3352 "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 3353 "dependencies": { 3354 "has-symbols": "^1.0.3" 3355 }, 3181 3356 "engines": { 3182 3357 "node": ">= 0.4" … … 3184 3359 "funding": { 3185 3360 "url": "https://github.com/sponsors/ljharb" 3361 } 3362 }, 3363 "node_modules/hasown": { 3364 "version": "2.0.4", 3365 "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", 3366 "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", 3367 "dependencies": { 3368 "function-bind": "^1.1.2" 3369 }, 3370 "engines": { 3371 "node": ">= 0.4" 3186 3372 } 3187 3373 }, … … 3233 3419 "engines": { 3234 3420 "node": ">= 0.8" 3421 } 3422 }, 3423 "node_modules/https-proxy-agent": { 3424 "version": "5.0.1", 3425 "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 3426 "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 3427 "dependencies": { 3428 "agent-base": "6", 3429 "debug": "4" 3430 }, 3431 "engines": { 3432 "node": ">= 6" 3235 3433 } 3236 3434 }, … … 3321 3519 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 3322 3520 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 3521 "dev": true, 3323 3522 "license": "ISC" 3324 3523 }, … … 3719 3918 } 3720 3919 }, 3721 "node_modules/li": {3722 "version": "1.3.0",3723 "resolved": "https://registry.npmjs.org/li/-/li-1.3.0.tgz",3724 "integrity": "sha512-z34TU6GlMram52Tss5mt1m//ifRIpKH5Dqm7yUVOdHI+BQCs9qGPHFaCUTIzsWX7edN30aa2WrPwR7IO10FHaw==",3725 "license": "MIT"3726 },3727 3920 "node_modules/locate-path": { 3728 3921 "version": "6.0.0", … … 3844 4037 } 3845 4038 }, 4039 "node_modules/math-intrinsics": { 4040 "version": "1.1.0", 4041 "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 4042 "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 4043 "engines": { 4044 "node": ">= 0.4" 4045 } 4046 }, 3846 4047 "node_modules/media-typer": { 3847 4048 "version": "0.3.0", … … 3875 4076 "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 3876 4077 "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 4078 "dev": true, 3877 4079 "license": "MIT", 3878 4080 "engines": { … … 3894 4096 } 3895 4097 }, 3896 "node_modules/mime": {3897 "version": "2.6.0",3898 "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",3899 "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",3900 "license": "MIT",3901 "bin": {3902 "mime": "cli.js"3903 },3904 "engines": {3905 "node": ">=4.0.0"3906 }3907 },3908 4098 "node_modules/mime-db": { 3909 4099 "version": "1.52.0", … … 3965 4155 "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 3966 4156 "license": "MIT" 4157 }, 4158 "node_modules/muggle-string": { 4159 "version": "0.3.1", 4160 "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", 4161 "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", 4162 "dev": true 3967 4163 }, 3968 4164 "node_modules/mute-stream": { … … 4069 4265 "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 4070 4266 "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 4267 "dev": true, 4071 4268 "license": "MIT", 4072 4269 "funding": { … … 4254 4451 } 4255 4452 }, 4256 "node_modules/parse-link-header": {4257 "version": "1.0.1",4258 "resolved": "https://registry.npmjs.org/parse-link-header/-/parse-link-header-1.0.1.tgz",4259 "integrity": "sha512-Z0gpfHmwCIKDr5rRzjypL+p93aHVWO7e+0rFcUl9E3sC67njjs+xHFenuboSXZGlvYtmQqRzRaE3iFpTUnLmFQ==",4260 "license": "MIT",4261 "dependencies": {4262 "xtend": "~4.0.1"4263 }4264 },4265 4453 "node_modules/parseurl": { 4266 4454 "version": "1.3.3", … … 4272 4460 "node": ">= 0.8" 4273 4461 } 4462 }, 4463 "node_modules/path-browserify": { 4464 "version": "1.0.1", 4465 "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", 4466 "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", 4467 "dev": true 4274 4468 }, 4275 4469 "node_modules/path-exists": { … … 4468 4662 }, 4469 4663 "node_modules/proxy-from-env": { 4470 "version": "1.1.0", 4471 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 4472 "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 4473 "license": "MIT" 4664 "version": "2.1.0", 4665 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", 4666 "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", 4667 "engines": { 4668 "node": ">=10" 4669 } 4474 4670 }, 4475 4671 "node_modules/punycode": { … … 4575 4771 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 4576 4772 "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 4773 "dev": true, 4577 4774 "license": "MIT", 4578 4775 "dependencies": { … … 4845 5042 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 4846 5043 "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 5044 "dev": true, 4847 5045 "funding": [ 4848 5046 { … … 5041 5239 "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 5042 5240 "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 5241 "dev": true, 5043 5242 "license": "MIT", 5044 5243 "dependencies": { … … 5128 5327 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 5129 5328 "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 5329 "dev": true, 5130 5330 "license": "MIT", 5131 5331 "dependencies": { … … 5172 5372 "funding": { 5173 5373 "url": "https://github.com/sponsors/sindresorhus" 5174 }5175 },5176 "node_modules/superagent": {5177 "version": "4.1.0",5178 "resolved": "https://registry.npmjs.org/superagent/-/superagent-4.1.0.tgz",5179 "integrity": "sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==",5180 "license": "MIT",5181 "dependencies": {5182 "component-emitter": "^1.2.0",5183 "cookiejar": "^2.1.2",5184 "debug": "^4.1.0",5185 "form-data": "^2.3.3",5186 "formidable": "^1.2.0",5187 "methods": "^1.1.1",5188 "mime": "^2.4.0",5189 "qs": "^6.6.0",5190 "readable-stream": "^3.0.6"5191 },5192 "engines": {5193 "node": ">= 6.0"5194 }5195 },5196 "node_modules/superagent/node_modules/form-data": {5197 "version": "2.5.1",5198 "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz",5199 "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==",5200 "license": "MIT",5201 "dependencies": {5202 "asynckit": "^0.4.0",5203 "combined-stream": "^1.0.6",5204 "mime-types": "^2.1.12"5205 },5206 "engines": {5207 "node": ">= 0.12"5208 }5209 },5210 "node_modules/superagent/node_modules/qs": {5211 "version": "6.11.2",5212 "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",5213 "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",5214 "license": "BSD-3-Clause",5215 "dependencies": {5216 "side-channel": "^1.0.4"5217 },5218 "engines": {5219 "node": ">=0.6"5220 },5221 "funding": {5222 "url": "https://github.com/sponsors/ljharb"5223 5374 } 5224 5375 }, … … 5519 5670 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 5520 5671 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 5672 "dev": true, 5521 5673 "license": "MIT" 5522 5674 }, … … 5730 5882 } 5731 5883 }, 5884 "node_modules/vue-template-compiler": { 5885 "version": "2.7.16", 5886 "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", 5887 "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", 5888 "dev": true, 5889 "dependencies": { 5890 "de-indent": "^1.0.2", 5891 "he": "^1.2.0" 5892 } 5893 }, 5894 "node_modules/vue-tsc": { 5895 "version": "1.8.27", 5896 "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", 5897 "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", 5898 "dev": true, 5899 "dependencies": { 5900 "@volar/typescript": "~1.11.1", 5901 "@vue/language-core": "1.8.27", 5902 "semver": "^7.5.4" 5903 }, 5904 "bin": { 5905 "vue-tsc": "bin/vue-tsc.js" 5906 }, 5907 "peerDependencies": { 5908 "typescript": "*" 5909 } 5910 }, 5732 5911 "node_modules/wcwidth": { 5733 5912 "version": "1.0.1", … … 5786 5965 } 5787 5966 }, 5788 "node_modules/wpapi": {5789 "version": "1.2.2",5790 "resolved": "https://registry.npmjs.org/wpapi/-/wpapi-1.2.2.tgz",5791 "integrity": "sha512-lkgi8Gjav3SArrCkNpG61ZnmCyamXKB+SjaR8tAoHhSZbJRTeabIlsdqUUAN3JGbVY3ht8p+EGdpCFIaanI5+w==",5792 "license": "MIT",5793 "dependencies": {5794 "li": "^1.3.0",5795 "parse-link-header": "^1.0.1",5796 "qs": "^6.6.0",5797 "superagent": "^4.0.0"5798 }5799 },5800 "node_modules/wpapi/node_modules/qs": {5801 "version": "6.11.2",5802 "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",5803 "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",5804 "license": "BSD-3-Clause",5805 "dependencies": {5806 "side-channel": "^1.0.4"5807 },5808 "engines": {5809 "node": ">=0.6"5810 },5811 "funding": {5812 "url": "https://github.com/sponsors/ljharb"5813 }5814 },5815 5967 "node_modules/wrap-ansi": { 5816 5968 "version": "7.0.0", … … 5846 5998 "engines": { 5847 5999 "node": ">=12" 5848 }5849 },5850 "node_modules/xtend": {5851 "version": "4.0.2",5852 "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",5853 "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",5854 "license": "MIT",5855 "engines": {5856 "node": ">=0.4"5857 6000 } 5858 6001 }, -
vtables/trunk/package.json
r3576926 r3754379 1 1 { 2 2 "name": "vtables", 3 "version": "0. 2.1",4 "description": " Create stunning, interactive tables effortlessly. No coding required. The ultimate WordPress plugin, lets you design powerful tables in a few clicks. Transform your data visualization now.",3 "version": "0.3", 4 "description": "Build interactive data tables from CSV, Excel and JSON files, or from scratch, and embed them anywhere with a shortcode.", 5 5 "productName": "vTables", 6 "author": "iamvs.com", 6 "author": "vTables", 7 "license": "GPL-3.0-only", 7 8 "private": true, 8 9 "scripts": { 9 10 "lint": "eslint --ext .js,.ts,.vue ./", 10 11 "format": "prettier --write \"**/*.{js,ts,vue,scss,html,md,json}\" --ignore-path .gitignore", 12 "typecheck": "vue-tsc --noEmit", 11 13 "test": "echo \"No test specified\" && exit 0", 12 14 "dev": "quasar dev", 13 "build": "quasar build ",15 "build": "quasar build && node scripts/patch-asset-base.mjs", 14 16 "release": "./release.sh" 15 17 }, … … 17 19 "@quasar/extras": "^1.0.0", 18 20 "@types/papaparse": "^5.3.9", 19 "@types/wpapi": "^1.1.1",20 21 "@vuelidate/core": "^2.0.3", 21 22 "@vuelidate/validators": "^2.0.3", … … 26 27 "pinia": "^2.0.11", 27 28 "quasar": "^2.6.0", 28 "rollbar": "^2.26.2",29 29 "vue": "^3.0.0", 30 30 "vue-i18n": "^9.2.2", 31 "vue-router": "^4.0.0", 32 "woocommerce-rest-ts-api": "^7.0.0", 33 "wpapi": "^1.2.2" 31 "vue-router": "^4.0.0" 34 32 }, 35 33 "devDependencies": { … … 45 43 "eslint-plugin-vue": "^9.0.0", 46 44 "prettier": "^2.5.1", 47 "typescript": "5.0.4" 45 "typescript": "5.0.4", 46 "vue-tsc": "^1.8.27" 48 47 }, 49 48 "engines": { -
vtables/trunk/public/css/vtables.css
r3212270 r3754379 1 :root { 2 --red: #f44336; 3 --red-1: #ffebee; 4 --red-2: #ffcdd2; 5 --red-3: #ef9a9a; 6 --red-4: #e57373; 7 --red-5: #ef5350; 8 --red-6: #f44336; 9 --red-7: #e53935; 10 --red-8: #d32f2f; 11 --red-9: #c62828; 12 --red-10: #b71c1c; 13 --red-11: #ff8a80; 14 --red-12: #ff5252; 15 --red-13: #ff1744; 16 --red-14: #d50000; 17 --pink: #e91e63; 18 --pink-1: #fce4ec; 19 --pink-2: #f8bbd0; 20 --pink-3: #f48fb1; 21 --pink-4: #f06292; 22 --pink-5: #ec407a; 23 --pink-6: #e91e63; 24 --pink-7: #d81b60; 25 --pink-8: #c2185b; 26 --pink-9: #ad1457; 27 --pink-10: #880e4f; 28 --pink-11: #ff80ab; 29 --pink-12: #ff4081; 30 --pink-13: #f50057; 31 --pink-14: #c51162; 32 --purple: #9c27b0; 33 --purple-1: #f3e5f5; 34 --purple-2: #e1bee7; 35 --purple-3: #ce93d8; 36 --purple-4: #ba68c8; 37 --purple-5: #ab47bc; 38 --purple-6: #9c27b0; 39 --purple-7: #8e24aa; 40 --purple-8: #7b1fa2; 41 --purple-9: #6a1b9a; 42 --purple-10: #4a148c; 43 --purple-11: #ea80fc; 44 --purple-12: #e040fb; 45 --purple-13: #d500f9; 46 --purple-14: #aa00ff; 47 --deep-purple: #673ab7; 48 --deep-purple-1: #ede7f6; 49 --deep-purple-2: #d1c4e9; 50 --deep-purple-3: #b39ddb; 51 --deep-purple-4: #9575cd; 52 --deep-purple-5: #7e57c2; 53 --deep-purple-6: #673ab7; 54 --deep-purple-7: #5e35b1; 55 --deep-purple-8: #512da8; 56 --deep-purple-9: #4527a0; 57 --deep-purple-10: #311b92; 58 --deep-purple-11: #b388ff; 59 --deep-purple-12: #7c4dff; 60 --deep-purple-13: #651fff; 61 --deep-purple-14: #6200ea; 62 --indigo: #3f51b5; 63 --indigo-1: #e8eaf6; 64 --indigo-2: #c5cae9; 65 --indigo-3: #9fa8da; 66 --indigo-4: #7986cb; 67 --indigo-5: #5c6bc0; 68 --indigo-6: #3f51b5; 69 --indigo-7: #3949ab; 70 --indigo-8: #303f9f; 71 --indigo-9: #283593; 72 --indigo-10: #1a237e; 73 --indigo-11: #8c9eff; 74 --indigo-12: #536dfe; 75 --indigo-13: #3d5afe; 76 --indigo-14: #304ffe; 77 --blue: #2196f3; 78 --blue-1: #e3f2fd; 79 --blue-2: #bbdefb; 80 --blue-3: #90caf9; 81 --blue-4: #64b5f6; 82 --blue-5: #42a5f5; 83 --blue-6: #2196f3; 84 --blue-7: #1e88e5; 85 --blue-8: #1976d2; 86 --blue-9: #1565c0; 87 --blue-10: #0d47a1; 88 --blue-11: #82b1ff; 89 --blue-12: #448aff; 90 --blue-13: #2979ff; 91 --blue-14: #2962ff; 92 --light-blue: #03a9f4; 93 --light-blue-1: #e1f5fe; 94 --light-blue-2: #b3e5fc; 95 --light-blue-3: #81d4fa; 96 --light-blue-4: #4fc3f7; 97 --light-blue-5: #29b6f6; 98 --light-blue-6: #03a9f4; 99 --light-blue-7: #039be5; 100 --light-blue-8: #0288d1; 101 --light-blue-9: #0277bd; 102 --light-blue-10: #01579b; 103 --light-blue-11: #80d8ff; 104 --light-blue-12: #40c4ff; 105 --light-blue-13: #00b0ff; 106 --light-blue-14: #0091ea; 107 --cyan: #00bcd4; 108 --cyan-1: #e0f7fa; 109 --cyan-2: #b2ebf2; 110 --cyan-3: #80deea; 111 --cyan-4: #4dd0e1; 112 --cyan-5: #26c6da; 113 --cyan-6: #00bcd4; 114 --cyan-7: #00acc1; 115 --cyan-8: #0097a7; 116 --cyan-9: #00838f; 117 --cyan-10: #006064; 118 --cyan-11: #84ffff; 119 --cyan-12: #18ffff; 120 --cyan-13: #00e5ff; 121 --cyan-14: #00b8d4; 122 --teal: #009688; 123 --teal-1: #e0f2f1; 124 --teal-2: #b2dfdb; 125 --teal-3: #80cbc4; 126 --teal-4: #4db6ac; 127 --teal-5: #26a69a; 128 --teal-6: #009688; 129 --teal-7: #00897b; 130 --teal-8: #00796b; 131 --teal-9: #00695c; 132 --teal-10: #004d40; 133 --teal-11: #a7ffeb; 134 --teal-12: #64ffda; 135 --teal-13: #1de9b6; 136 --teal-14: #00bfa5; 137 --green: #4caf50; 138 --green-1: #e8f5e9; 139 --green-2: #c8e6c9; 140 --green-3: #a5d6a7; 141 --green-4: #81c784; 142 --green-5: #66bb6a; 143 --green-6: #4caf50; 144 --green-7: #43a047; 145 --green-8: #388e3c; 146 --green-9: #2e7d32; 147 --green-10: #1b5e20; 148 --green-11: #b9f6ca; 149 --green-12: #69f0ae; 150 --green-13: #00e676; 151 --green-14: #00c853; 152 --light-green: #8bc34a; 153 --light-green-1: #f1f8e9; 154 --light-green-2: #dcedc8; 155 --light-green-3: #c5e1a5; 156 --light-green-4: #aed581; 157 --light-green-5: #9ccc65; 158 --light-green-6: #8bc34a; 159 --light-green-7: #7cb342; 160 --light-green-8: #689f38; 161 --light-green-9: #558b2f; 162 --light-green-10: #33691e; 163 --light-green-11: #ccff90; 164 --light-green-12: #b2ff59; 165 --light-green-13: #76ff03; 166 --light-green-14: #64dd17; 167 --lime: #cddc39; 168 --lime-1: #f9fbe7; 169 --lime-2: #f0f4c3; 170 --lime-3: #e6ee9c; 171 --lime-4: #dce775; 172 --lime-5: #d4e157; 173 --lime-6: #cddc39; 174 --lime-7: #c0ca33; 175 --lime-8: #afb42b; 176 --lime-9: #9e9d24; 177 --lime-10: #827717; 178 --lime-11: #f4ff81; 179 --lime-12: #eeff41; 180 --lime-13: #c6ff00; 181 --lime-14: #aeea00; 182 --yellow: #ffeb3b; 183 --yellow-1: #fffde7; 184 --yellow-2: #fff9c4; 185 --yellow-3: #fff59d; 186 --yellow-4: #fff176; 187 --yellow-5: #ffee58; 188 --yellow-6: #ffeb3b; 189 --yellow-7: #fdd835; 190 --yellow-8: #fbc02d; 191 --yellow-9: #f9a825; 192 --yellow-10: #f57f17; 193 --yellow-11: #ffff8d; 194 --yellow-12: #ffff00; 195 --yellow-13: #ffea00; 196 --yellow-14: #ffd600; 197 --amber: #ffc107; 198 --amber-1: #fff8e1; 199 --amber-2: #ffecb3; 200 --amber-3: #ffe082; 201 --amber-4: #ffd54f; 202 --amber-5: #ffca28; 203 --amber-6: #ffc107; 204 --amber-7: #ffb300; 205 --amber-8: #ffa000; 206 --amber-9: #ff8f00; 207 --amber-10: #ff6f00; 208 --amber-11: #ffe57f; 209 --amber-12: #ffd740; 210 --amber-13: #ffc400; 211 --amber-14: #ffab00; 212 --orange: #ff9800; 213 --orange-1: #fff3e0; 214 --orange-2: #ffe0b2; 215 --orange-3: #ffcc80; 216 --orange-4: #ffb74d; 217 --orange-5: #ffa726; 218 --orange-6: #ff9800; 219 --orange-7: #fb8c00; 220 --orange-8: #f57c00; 221 --orange-9: #ef6c00; 222 --orange-10: #e65100; 223 --orange-11: #ffd180; 224 --orange-12: #ffab40; 225 --orange-13: #ff9100; 226 --orange-14: #ff6d00; 227 --deep-orange: #ff5722; 228 --deep-orange-1: #fbe9e7; 229 --deep-orange-2: #ffccbc; 230 --deep-orange-3: #ffab91; 231 --deep-orange-4: #ff8a65; 232 --deep-orange-5: #ff7043; 233 --deep-orange-6: #ff5722; 234 --deep-orange-7: #f4511e; 235 --deep-orange-8: #e64a19; 236 --deep-orange-9: #d84315; 237 --deep-orange-10: #bf360c; 238 --deep-orange-11: #ff9e80; 239 --deep-orange-12: #ff6e40; 240 --deep-orange-13: #ff3d00; 241 --deep-orange-14: #dd2c00; 242 --brown: #795548; 243 --brown-1: #efebe9; 244 --brown-2: #d7ccc8; 245 --brown-3: #bcaaa4; 246 --brown-4: #a1887f; 247 --brown-5: #8d6e63; 248 --brown-6: #795548; 249 --brown-7: #6d4c41; 250 --brown-8: #5d4037; 251 --brown-9: #4e342e; 252 --brown-10: #3e2723; 253 --brown-11: #d7ccc8; 254 --brown-12: #bcaaa4; 255 --brown-13: #8d6e63; 256 --brown-14: #5d4037; 257 --grey: #9e9e9e; 258 --grey-1: #fafafa; 259 --grey-2: #f5f5f5; 260 --grey-3: #eeeeee; 261 --grey-4: #e0e0e0; 262 --grey-5: #bdbdbd; 263 --grey-6: #9e9e9e; 264 --grey-7: #757575; 265 --grey-8: #616161; 266 --grey-9: #424242; 267 --grey-10: #212121; 268 --grey-11: #f5f5f5; 269 --grey-12: #eeeeee; 270 --grey-13: #bdbdbd; 271 --grey-14: #616161; 272 --blue-grey: #607d8b; 273 --blue-grey-1: #eceff1; 274 --blue-grey-2: #cfd8dc; 275 --blue-grey-3: #b0bec5; 276 --blue-grey-4: #90a4ae; 277 --blue-grey-5: #78909c; 278 --blue-grey-6: #607d8b; 279 --blue-grey-7: #546e7a; 280 --blue-grey-8: #455a64; 281 --blue-grey-9: #37474f; 282 --blue-grey-10: #263238; 283 --blue-grey-11: #cfd8dc; 284 --blue-grey-12: #b0bec5; 285 --blue-grey-13: #78909c; 286 --blue-grey-14: #455a64; 287 } 288 289 body.q-body--prevent-scroll { 290 position: initial !important; 291 } 292 1 293 #wpcontent { 2 294 padding: 20px; … … 5 297 .gap-5 { 6 298 gap: 5px; 299 } 300 301 .gap-10 { 302 gap: 10px; 7 303 } 8 304 … … 40 336 } 41 337 42 #adminmenu #toplevel_page_vtables .wp-menu-image img { 43 opacity: 1; 44 } 338 .q-table td, 339 .q-table th { 340 white-space: normal; 341 word-wrap: break-word; 342 } 343 344 #q-notify .row { 345 padding-top: 0; 346 padding-bottom: 0; 347 } 348 349 /* ===== TheTable styles ===== */ 350 351 .q-item__section--avatar { 352 min-width: unset; 353 } 354 355 .settings-open { 356 margin-right: 325px; 357 } 358 359 .fullscreen .q-table__top { 360 padding: 0; 361 } 362 363 .fullscreen .q-table__top .vtable__top { 364 padding: 12px 16px; 365 } 366 367 .fullscreen.q-table--dense .vtable__top { 368 padding: 8px 16px; 369 } 370 371 .cell-value { 372 display: block; 373 white-space: initial; 374 width: inherit; 375 } 376 377 .q-table--cell-separator .q-table__top { 378 border-bottom: none !important; 379 } 380 381 .q-table--cell-separator .q-table__top .vtable__top { 382 border-bottom: 1px solid rgba(0, 0, 0, 0.12); 383 } 384 385 .vtable { 386 margin: 10px 0; 387 } 388 389 .vtable .q-table__bottom { 390 padding: 0; 391 } 392 393 .vtable .q-table__bottom > div { 394 border-radius: 0 0 4px 4px; 395 padding: 4px 10px 4px 10px; 396 min-height: inherit; 397 } 398 399 .vtable .q-table__bottom--nodata { 400 padding-left: 10px; 401 padding-right: 10px; 402 } 403 404 .vtable .q-field__bottom { 405 padding-left: 0; 406 padding-right: 0; 407 position: absolute; 408 background: #fff; 409 } 410 411 .vtable .q-table__top { 412 padding: 0; 413 overflow: hidden; 414 } 415 416 .vtable .q-table__top > div { 417 padding: 12px 16px; 418 min-height: inherit; 419 } 420 421 .vtable .q-table--dense .q-table__top { 422 padding: 0; 423 overflow: hidden; 424 } 425 426 .vtable .q-table--dense .q-table__top > div { 427 padding: 8px 16px; 428 min-height: inherit; 429 } 430 431 .edit-icon { 432 position: absolute; 433 } 434 .q-td .edit-icon { 435 top: -8px; 436 } 437 /* Quasar makes body cells positioned, header cells not — the edit icon needs 438 the th as its containing block. */ 439 .q-table thead th { 440 position: relative; 441 } 442 .q-table thead th .edit-icon { 443 top: -6px; 444 } 445 .vtable__top h1 { 446 display: inline-block; 447 position: relative; 448 } 449 .vtable__top h1 .edit-icon { 450 top: -10px; 451 right: -45px; 452 } 453 .vtable__top:hover .edit-icon, 454 .q-td:hover .edit-icon, 455 .q-table thead th:hover .edit-icon, 456 .q-item__section:hover .edit-icon { 457 display: block !important; 458 } 459 460 .admin-bar-presented-on-fullscreen { 461 margin-top: 30px; 462 margin-left: 160px; 463 } 464 465 .fullscreen-btn { 466 height: 40px; 467 width: 40px; 468 } 469 470 .v-enter-active, 471 .v-leave-active { 472 transition: opacity 0.5s ease; 473 } 474 475 .v-enter-from, 476 .v-leave-to { 477 opacity: 0; 478 } 479 480 .q-table th.sortable { 481 cursor: pointer; 482 min-width: 116px; 483 } 484 485 .cell-raw-html > * { 486 cursor: pointer; 487 } 488 489 .q-table__container { 490 margin: 2px; 491 } 492 493 .pre-wrap { 494 white-space: pre-wrap; 495 } 496 497 .flex-no-wrap { 498 flex-wrap: nowrap; 499 } -
vtables/trunk/quasar.config.js
r3036822 r3754379 55 55 56 56 vueRouterMode: 'hash', // available values: 'hash', 'history' 57 // vueRouterBase, 57 58 // Empty on purpose: with the hash router the base only pollutes generated 59 // hrefs, and leaving it unset would make Quasar derive it from publicPath 60 // below — i.e. bake the placeholder token into the router too. 61 vueRouterBase: '', 62 58 63 // vueDevtools, 59 64 // vueOptionsAPI: false, … … 61 66 // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup 62 67 63 publicPath: ctx.dev ? '/' : './wp-content/plugins/vtables/dist/spa', 68 // Placeholder, NOT a real path. Vite 2.9 inlines the public path as a 69 // string literal into the bundle (lazy-chunk base, CSS url()), so a real 70 // path here would hardcode the install location and break sites where WP 71 // lives in a subdirectory, wp-content is relocated, or the plugin folder 72 // is renamed. scripts/patch-asset-base.mjs rewrites this token after the 73 // build into a runtime-resolved base — see the script's header. 74 publicPath: ctx.dev ? '/' : '/__vtables_base__/', 64 75 // analyze: true, 65 76 // env: {}, … … 95 106 // https: true 96 107 open: false, // opens browser window automatically 97 port: 9001, 108 // Must match VTABLES_BASE_URL in env.php. Kept apart from vTables Pro's 109 // 9001: Quasar silently moves to the next free port when this one is 110 // taken, and WordPress would then load the other plugin's dev bundle. 111 port: 9000, 112 proxy: { 113 '/wp-json': { 114 target: 'http://localhost:8000', 115 changeOrigin: true, 116 }, 117 }, 98 118 }, 99 119 100 120 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework 101 121 framework: { 102 config: {}, 122 config: { 123 notify: { 124 position: 'bottom-right', 125 }, 126 }, 103 127 104 128 // iconSet: 'material-icons', // Quasar icon set … … 122 146 // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles 123 147 sourceFiles: { 124 rootComponent: 'src/domains/general/ui/App.vue',125 store: 'src/domains/general/store/index',126 router: 'src/domains/general/router/index',148 // rootComponent: 'src/domains/general/ui/App.vue', 149 // store: 'src/domains/general/store/index', 150 // router: 'src/domains/general/router/index', 127 151 // registerServiceWorker: 'src-pwa/register-service-worker', 128 152 // serviceWorker: 'src-pwa/custom-service-worker', -
vtables/trunk/readme.txt
r3576934 r3754379 1 === Data table plugin: Spreadsheets | Google Sheets | WooCommerce product tables===1 === vTables – Data Tables from CSV, Excel and JSON === 2 2 Contributors: iamvs 3 Author URI: https://iamvs.com/4 Plugin URI: https://vtables.pro/5 3 Donate link: https://vtables.pro/ 6 Tags: data table, spreadsheet, google sheets, woocommerce-table, product-table7 Requires at least: 4.78 Tested up to: 7. 09 Stable tag: 0.2.1 10 Requires PHP: 7.0 4 Tags: table, data table, spreadsheet, csv, excel 5 Requires at least: 6.0 6 Tested up to: 7.1 7 Requires PHP: 7.4 8 Stable tag: 0.3 11 9 License: GPLv3 12 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html 13 11 14 WordPress data table plugin: spreadsheets, google sheets, WooCommerce product tables. Transform your data into stunning data tables.12 Build interactive data tables from CSV, Excel and JSON files, or from scratch, and embed them anywhere with a shortcode. 15 13 16 14 == Description == 17 15 18 [Website](https://vtables.pro/) | [Video Guide](https://www.youtube.com/watch?v=RRMnKfhCut8) | [User Guide](https://vtables.pro/documentation) | [FAQ](https://vtables.pro/faq) | [Developer](https://iamvs.com) 16 vTables turns spreadsheets into clean, responsive tables for your site. Import a file or start from an empty grid, edit it right in the WordPress admin, style it, and place it on any page or post with a shortcode. 19 17 20 Unlock Google Sheets and WooCommerce integrations with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro)18 [Website](https://vtables.pro/) | [User Guide](https://vtables.pro/documentation) | [FAQ](https://vtables.pro/faq) 21 19 22 Discover the ultimate solution for transforming your WordPress site's data presentation with our spreadsheet plugin. Seamlessly convert spreadsheets, Google Sheets, and WooCommerce products into captivating tables that captivate your audience effortlessly. 20 = Features = 23 21 24 https://www.youtube.com/watch?v=RRMnKfhCut8 22 * **Import** CSV, JSON, XLS and XLSX files (large files are uploaded in chunks and imported in the background). 23 * **Build tables from scratch**: set the number of columns and rows, then fill the cells in. 24 * **Edit in place**: double-click any cell or header to edit it; add, rename and delete rows and columns from the right-click menu. 25 * **Unlimited tables**, and as many tables per page as you like. 26 * **Shortcode embed**: `[vtables id="1"]` shows the table; `[vtables id="1" row="2" column="email"]` prints a single cell value inside your text. 27 * **Smart cells**: image URLs become thumbnails, links become clickable, `<button href="…">Label</button>` becomes a button, and basic HTML (links, images, line breaks) is rendered safely. 28 * **Style**: colors for the title bar, header, body, pagination bar, links and buttons; dense and flat layouts; row/column separators; sticky header; table height and maximum width. 29 * **Pagination** for large tables: rows are loaded page by page from the database, so tables with many thousands of rows stay fast. 30 * **Export** any table as CSV, JSON or XLSX. 31 * Works with the block editor, classic editor and page builders that accept shortcodes (Divi, Elementor, WPBakery, …). 32 * Translated into English, German, Spanish, French, Hindi, Italian, Norwegian, Portuguese, Russian, Swedish, Turkish and Chinese. 25 33 26 = == Data sources ===34 = vTables Pro = 27 35 28 1. CSV spreadsheet; 29 2. JSON data; 30 2. Google Sheets; 31 3. WooCommerce products. 36 [vTables Pro](https://vtables.pro) is a separate, paid add-on that runs on top of this plugin and uses the same tables. It adds: 32 37 33 === Features === 38 * Data sources: Google Sheets, external CSV/JSON URLs and WooCommerce product tables, with automatic sync. 39 * Visitor tools: search, sorting and row filters. 40 * Layout: column visibility, sticky first/last column, column widths, grouped headers, responsive card view, rows per page, custom CSS. 41 * More cell formats: video, PDF, audio, Google Maps, star rating, progress bar and JSON. 34 42 35 1. Unlimited Tables: 36 Create and customize data tables to your heart's content. There are no limits here – whether you're managing a small business or curating content, vTables empowers you to showcase your data in unlimited ways. 🌟 43 This plugin is fully functional on its own and never requires Pro. 37 44 38 2. Unlimited Tables per Page: 39 Break free from layout constraints and seamlessly display multiple tables on a single page. With vTables, your users can explore a wealth of information without any hindrance. 🚀 45 = Source code and third-party libraries = 40 46 41 3. Extract specific values from a sheet 42 Effortlessly retrieve specific cell values from your spreadsheets using custom attributes within a shortcode. For example, [vtables id="1" row="1" column="email"] lets you grab exactly what you need from your data. 💡 47 The admin screen and the embedded table are a Vue application. Its complete, unminified source ships with the plugin in the `src/` folder, together with the build setup (`package.json`, `quasar.config.js`, `scripts/`). To rebuild: `npm ci && npm run build` (PHP dependencies: `composer install --no-dev`). 43 48 44 4. Google Sheets Integration 45 Keep your WordPress data tables in sync with Google Sheets effortlessly. With just a click, your tables reflect any updates made to your spreadsheet. 🎶 46 *available in **vTables Pro Add-on** – [Download for FREE](https://vtables.pro)* 49 Bundled libraries, all GPL-compatible: 47 50 48 5. WooCommerce Integration 49 Enhance your e-commerce strategy by seamlessly managing and displaying product information. Connect your WooCommerce shop with vTables for a cohesive user experience. 🌟 50 *available in **vTables Pro Add-on** – [Download for FREE](https://vtables.pro)* 51 * Vue, Vue Router, Pinia, Vue I18n (MIT) 52 * Quasar Framework (MIT) and Material Icons (Apache License 2.0) 53 * Axios, Lodash, Papa Parse, VueUse, Vuelidate (MIT) 54 * SimpleXLSX and SimpleXLS by Sergey Shuchkin (MIT) 51 55 52 6. Styling 53 Customize your data tables to match your site's design seamlessly. With extended styling options, choose color schemes, and more to create visually stunning tables. 🎨 56 vTables does not track users and makes no requests to external services. The only external links in the admin are to vtables.pro, and they open only when you click them. 54 57 55 7. Export 56 Prepare reports, share insights, or collaborate with your team by seamlessly exporting tables in various formats such as CSV and JSON. 🔗 57 *available in **vTables Pro Add-on** – [Download for FREE](https://vtables.pro)* 58 == Installation == 58 59 59 8. Premium Features: 60 Unlock exclusive premium features like sorting, search, sticky columns, and more to enhance user interaction. 61 *available for [PREMIUM](https://vtables.pro/pricing/) plans only* 62 63 9. vTables seamlessly integrates with leading site builders such as Divi, Elementor, and WPBakery. 64 Whether you're utilizing Divi table, Elementor table, or WPBakery table, vTables seamlessly integrates with leading site builders. Incorporate tables, CSV spreadsheets, and Google Sheets effortlessly into your site's design. 65 66 67 == Why Choose vTables (PRO): Spreadsheets, Google Sheets, WooCommerce product tables == 68 69 - Simplifies complex data management tasks. 70 - Boosts productivity with efficient spreadsheet tools. 71 - Perfect for store owners looking to streamline their operations. 72 - Import and export product data using CSV and JSON. 73 - Seamless integration with Google Sheets. 74 - Seamless integration with WooCommerce. 75 - Create and manage WooCommerce product tables effortlessly. 76 - Supports multiple spreadsheet formats and customization options. 77 - User-friendly interface with drag-and-drop functionality. 78 79 80 == Requirements == 81 82 – WordPress version 6.4 or higher 83 – PHP version 8.0 or higher 84 – Browser compatibility: Edge >= 88, Firefox >= 78, Chrome >= 87, Safari >= 13.1 85 86 87 == Notice == 88 If you're using a caching plugin, make sure to exclude the vTables plugin from caching. In your cache plugin settings, locate the **"Do Not Cache URIs"** field and add **"vtables"** to the list. After making this change, clear the cache to ensure the new settings take effect. 89 If you've made a change and don't see updates on the page, try re-saving it to trigger a page-level cache reset, or clear the cache manually through your caching plugin. 90 60 1. In the WordPress admin go to **Plugins → Add New**, search for **vTables**, and click **Install Now**, then **Activate**. (Or upload the `vtables` folder to `/wp-content/plugins/` and activate it on the Plugins screen.) 61 2. Open **vTables** in the admin menu and click **Add table**. 62 3. Choose **Create** to start from an empty grid, or **Upload** to import a CSV, JSON, XLS or XLSX file. 63 4. Copy the table's shortcode (for example `[vtables id="1"]`) and paste it into any page or post. 91 64 92 65 == Frequently Asked Questions == 93 66 94 = Can users without a technical background or development experience use the vTables? =67 = How do I show a table on a page? = 95 68 96 vTables is specifically designed to be the most beginner-friendly and non-technical WordPress table plugin available. Our documentation and user manuals are meticulously crafted to ensure that even individuals with limited technical proficiency can harness the full potential of our table builder effortlessly.69 Every table has a shortcode, shown in the table list and on the table's page, like `[vtables id="1"]`. Paste it into a Shortcode block (or directly into the classic editor or your page builder). 97 70 98 = Who is vTables plugin suitable for? =71 = How do I print a single value from a table? = 99 72 100 vTables – is a [WordPress table plugin](https://vtables.pro) that caters to a broad audience, from beginners to experts. The user-friendly interface empowers anyone to comfortably create and customize dynamic tables without the need for coding skills.73 Add `row` and `column`: `[vtables id="1" row="2" column="email"]` prints the value of the `email` column in the second row. Column names are case-insensitive and spaces become underscores. 101 74 102 = Is it possible to import tables from other plugins into vTables? =75 = Can I upload an Excel file? = 103 76 104 Certainly, you can seamlessly import tables from other plugins into vTables by exporting them in CSV format.77 Yes. XLSX and XLS files are imported directly; the first row becomes the header. 105 78 106 = How can vTables handle long tables with a substantial amount of data? =79 = Can I put images, links and buttons in cells? = 107 80 108 Managing extensive tables with large datasets is made simple with vTables. You can utilize features such as Pagination to break down tables into segments. Additionally, the Search/Filter option enables quick retrieval of specific information from even the most colossal table datasets.81 Yes. Paste an image URL (ending in .jpg, .png, .gif, .webp, .svg, …) to show a thumbnail — clicking it opens the full image — or any http(s) link to show a clickable link. For a button, use `<button href="https://example.com">Buy now</button>`. 109 82 110 = Can I import tables from Google Sheet or Google Drive? =83 = I updated from vTables 0.2.x. What changed? = 111 84 112 Y es, vTables allows you to import tables from Google Sheet / Google Drive with the added functionality provided by the **vTables Pro Add-on** – [Download for FREE](https://vtables.pro).85 Your tables, their data and their shortcodes keep working, and the update runs automatically in the background: 113 86 114 = How to upload an EXCEL file? = 87 * Table rows are moved into a dedicated database table, a few tables at a time, so large tables load page by page. 88 * Some settings that were free in 0.2.x — search, sorting, sticky columns, custom CSS and rows per page — are now part of vTables Pro. Your saved values are kept untouched; they apply again if you install vTables Pro. Until then the table shows with the free options, and a one-time notice lists how many tables are affected. 89 * Columns you had hidden stay hidden. 115 90 116 To upload an EXCEL file, save it as a CSV. To do this, open your spreadsheet in EXCEL, click "File" -> "Save As," and select the "CSV" format. After that, you will be able to upload and convert this file into a table. 91 = Does vTables work with caching plugins? = 117 92 118 = Can I upload images to table cells? = 93 Yes. The embedded table is served from its own URL (`/vtables-embed/{id}/`) that tells caching and optimisation plugins not to cache or rewrite it. If a page still shows old content after an update, clear your page cache once. 119 94 120 You can simply paste a URL (it should contain an image extension) to an image into the desired cell, and the plugin will do the rest. 121 You can also turn on the "Image Popup" option in the settings to show an image in a popup when clicked. 95 = What is the relationship with vTables Pro? = 122 96 123 Currently available with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro).97 vTables Pro is an optional paid add-on. It needs this plugin to be active and works on the same tables, so you can install it at any time without re-creating anything — and if you remove it later, your file-based tables keep working here. 124 98 125 = Can I add buttons to table cells? =99 = Is my data deleted when I delete the plugin? = 126 100 127 Yes, you can simply paste a link into the desired cell, and the plugin will do the rest. You can also convert your links into buttons. To do this, simply switch the toggle "Links as" to the "Button" option. [available with PREMIUM plans](https://vtables.pro/pricing) 128 129 = Can I connect Google Sheets to the plugin? = 130 131 You can easily connect Google Sheets with your tables. 132 133 Available with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro). 134 135 = Can I import a spreadsheet from Google Sheets? = 136 137 Yes, you can easily import Google Sheets into the plugin. 138 139 Available with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro). 140 141 = Can I create a WooCommerce product table? = 142 143 Sure, you can create a product table in just a few clicks. On the "Tables" page, click the "Add Table" button, select "Woo Products," name your table, select the desired category/categories with products, and click "Import." 144 145 Available with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro). 146 147 = Can I create a WooCommerce product table with external products? = 148 149 Sure, you can create a product table with external products in just a few clicks. On the "Tables" page, click the "Add Table" button, select "Woo Products," name your table, select the desired category/categories with products, and click "Import." Alternatively, if you haven't set up WooCommerce, you can create a spreadsheet or Google Sheet with external links and import that sheet. 150 151 Available with **vTables Pro Add-on** – [Download for FREE](https://vtables.pro). 101 No. Deleting the plugin removes its settings, but keeps your tables, so reinstalling brings them back. To delete the tables as well, add `define( 'VTABLES_REMOVE_ALL_DATA', true );` to wp-config.php before deleting the plugin (this is ignored while vTables Pro is installed, since it uses the same tables). 152 102 153 103 = Can I suggest a feature? = 154 104 155 Sure , please use the contact form on the official website.105 Sure — please use the contact form on [vtables.pro](https://vtables.pro). 156 106 157 107 == Screenshots == 158 108 159 1. "Dashboard" 160 2. "CSV spreadsheet" 161 3. "Google Sheets" 162 4. "WooCommerce product table" 163 5. "Table settings" 109 1. Tables list 110 2. A table imported from a CSV file 111 3. Google Sheets table (vTables Pro) 112 4. WooCommerce product table (vTables Pro) 113 5. Table settings 114 115 == Upgrade Notice == 116 117 = 3.0.0 = 118 Rebuilt admin and table viewer, faster pagination, XLS/XLSX import and security hardening. Tables and shortcodes keep working; search, sorting, sticky columns, custom CSS and rows per page move to vTables Pro, and their saved values are kept. 164 119 165 120 == Changelog == 121 122 = 3.0.0 = 123 * New admin and table viewer, with in-place editing of cells, headers, rows and columns. 124 * New: XLS and XLSX import; chunked background import for large files. 125 * New: tables are stored row by row and paged from the database; existing tables are migrated in the background. 126 * New: export as XLSX. 127 * New: the embedded table is served by WordPress (`/vtables-embed/{id}/`) and opts out of page caching and script optimisation. 128 * Security: all admin actions require a nonce and the `manage_options` capability; the public endpoints only read single tables; SQL ordering is allowlisted; database errors are no longer shown to visitors; uploads in progress are stored in a protected folder. 129 * Changed: search, sorting, sticky columns, custom CSS and rows per page are now vTables Pro features; saved values are kept. 130 * Removed: the `vtables/v1` REST routes (replaced by admin-ajax actions). 166 131 167 132 = 0.2.1 = -
vtables/trunk/src/boot/i18n.ts
r3576926 r3754379 3 3 4 4 import messages from 'src/i18n'; 5 import { getSysInfo } from 'boot/wpapi'; 5 6 6 7 export type MessageLanguages = keyof typeof messages; 7 8 // Type-define 'en-US' as the master schema for the resource 8 export type MessageSchema = typeof messages['en-US'];9 export type MessageSchema = (typeof messages)['en-US']; 9 10 10 11 // See https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition … … 22 23 /* eslint-enable @typescript-eslint/no-empty-interface */ 23 24 25 function resolveLocale(): MessageLanguages { 26 const wpLocale: string = getSysInfo().locale ?? ''; 27 28 if (wpLocale.startsWith('es')) return 'es'; 29 if (wpLocale.startsWith('de')) return 'de'; 30 if (wpLocale.startsWith('fr')) return 'fr'; 31 if (wpLocale.startsWith('it')) return 'it'; 32 if (wpLocale.startsWith('ru')) return 'ru'; 33 if (wpLocale.startsWith('pt')) return 'pt'; 34 if (wpLocale.startsWith('hi')) return 'hi'; 35 if (wpLocale.startsWith('nb')) return 'nb'; 36 if (wpLocale.startsWith('sv')) return 'sv'; 37 if (wpLocale.startsWith('tr')) return 'tr'; 38 if (wpLocale === 'zh_CN') return 'zh-CN'; 39 if (wpLocale.startsWith('zh')) return 'zh-HK'; 40 return 'en-US'; 41 } 42 24 43 export default boot(({ app }) => { 25 44 const i18n = createI18n({ 26 locale: 'en-US', 45 locale: resolveLocale(), 46 fallbackLocale: 'en-US', 27 47 legacy: false, 28 48 messages, -
vtables/trunk/src/boot/services.ts
r3576926 r3754379 1 import { api } from 'src/ domains/general/services';1 import { api } from 'src/services'; 2 2 3 3 export { api }; -
vtables/trunk/src/boot/wpapi.ts
r3576926 r3754379 1 import WP from 'wpapi'; 1 /** 2 * WordPress-provided runtime values, localized as `vtables_sys_info`: 3 * - admin screen: wp_localize_script in LoadAssets::vtables_enqueue_assets_admin 4 * (ajaxurl, nonce, upload limit, locale); 5 * - public embed document: printed inline by Embed::render_document 6 * (ajaxurl, locale — never a nonce). 7 * The SPA reads its own window first; the parent window is a fallback for 8 * embeds cached before the values were printed into the document. 9 */ 10 export type SysInfo = { 11 ajaxurl?: string; 12 nonce?: string; 13 max_upload_size?: number | string; 14 locale?: string; 15 plain_links?: boolean | string; 16 pro_url?: string; 17 }; 2 18 3 const windowObj: any = window; 19 export function getSysInfo(): SysInfo { 20 type WithSysInfo = { vtables_sys_info?: SysInfo }; 21 const own = (window as unknown as WithSysInfo).vtables_sys_info; 22 if (own) return own; 4 23 5 const baseUrl = windowObj.WP_API_Settings?.base_url || '/wp-json/'; 24 try { 25 return (window.parent as unknown as WithSysInfo)?.vtables_sys_info ?? {}; 26 } catch { 27 // Cross-origin parent: nothing to read. 28 return {}; 29 } 30 } 6 31 7 const wpapi = new WP({ 8 endpoint: baseUrl, 9 nonce: windowObj.WP_API_Settings.nonce, 10 }); 32 function getAjaxUrl(): string { 33 return getSysInfo().ajaxurl || '/wp-admin/admin-ajax.php'; 34 } 11 35 12 wpapi.vtables = wpapi.registerRoute('vtables/v1', '/tables/(?P<id>\\d+)'); 36 function getSecurity(): string { 37 return getSysInfo().nonce || ''; 38 } 13 39 14 wpapi.vtables_source = wpapi.registerRoute( 15 'vtables/v1', 16 '/source/(?P<id>\\d+)' 17 ); 40 function getUploadMaxFilesize(): number { 41 const size = Number(getSysInfo().max_upload_size); 42 // Conservative fallback: 2 MB 43 return size > 0 ? size : 2 * 1024 * 1024; 44 } 18 45 19 wpapi.vtables_settings = wpapi.registerRoute( 20 'vtables/v1', 21 '/settings/(?P<id>\\d+)'22 ); 46 /** Where the "Pro" teasers link to. */ 47 function getProUrl(): string { 48 return getSysInfo().pro_url || 'https://vtables.pro'; 49 } 23 50 24 wpapi.license = wpapi.registerRoute('vtables/v1', '/license');51 const ajaxUrl = getAjaxUrl(); 25 52 26 export { wpapi, baseUrl };53 export { ajaxUrl, getSecurity, getUploadMaxFilesize, getProUrl }; -
vtables/trunk/src/css/app.scss
r3576926 r3754379 1 @import url('/public/css/vtables.css'); 1 .material-icons { 2 font-family: Material Icons; 3 font-weight: 400; 4 font-style: normal; 5 display: inline-block; 6 line-height: 1; 7 text-transform: none; 8 letter-spacing: normal; 9 word-wrap: normal; 10 white-space: nowrap; 11 direction: ltr; 12 -webkit-font-smoothing: antialiased; 13 text-rendering: optimizeLegibility; 14 -moz-osx-font-smoothing: grayscale; 15 font-feature-settings: 'liga'; 16 } -
vtables/trunk/src/i18n/en-US/index.ts
r3576926 r3754379 5 5 abort_upload: 'Abort Upload', 6 6 actions: 'Actions', 7 activate: 'Activate', 7 8 active: 'Active', 9 inactive: 'Inactive', 10 and: 'and', 11 add_column: 'Add column', 8 12 add_column_after: 'Add column after', 9 13 add_column_before: 'Add column before', 14 add_condition: 'Add condition', 15 add_group: 'Add group', 16 add_row: 'Add row', 10 17 add_row_after: 'Add row after', 11 18 add_row_before: 'Add row before', 19 add_subgroup: 'Add subgroup', 12 20 add_table: 'Add table', 21 add_to_cart: 'Add to cart', 22 add_to_cart_button_label: 'Add to cart button label', 13 23 add_top_row: 'Add top row', 24 appearence: 'Appearence', 25 applied: 'Applied', 26 apply: 'Apply', 27 apply_changes: 'Apply changes', 28 ascending: 'Ascending', 29 auto_sync: 'Auto Sync', 30 auto_sync_interval: 'Auto-sync interval', 31 manual_sync: 'Manual sync', 14 32 back_to_dashboard: 'Back to dashboard', 15 33 background_color: 'Background color', 16 34 baseline: 'Baseline', 17 35 binary_sorting: 'Binary sorting', 36 body: 'Body', 18 37 bordered: 'Bordered', 19 38 borderless: 'Borderless', … … 26 45 button_text: 'Button text', 27 46 button_text_color: 'Button text color', 47 buy_now: 'Buy now', 48 buy_now_mode: 'Buy now mode', 28 49 cancel: 'Cancel', 29 50 card_css_class: 'Card CSS class', … … 32 53 clear: 'Clear', 33 54 clear_all: 'Clear All', 55 clear_group_tree: 'Clear tree', 56 close: 'Close', 34 57 color: 'Color', 35 58 column: 'Column', 59 column_groups: 'Col. groups', 60 column_name: 'Column Name', 36 61 columns: 'Columns', 37 62 columns_max_width: 'Columns max width', 63 columns_min_width: 'Columns min width', 64 columns_desktop: 'Columns (desktop)', 65 columns_mobile: 'Columns (mobile)', 66 columns_tablet: 'Columns (tablet)', 38 67 confirm: 'Confirm', 68 connected: 'Connected', 39 69 content: 'Content', 40 70 copied: 'Copied', 41 71 copy: 'Copy', 72 create: 'Create', 73 create_table: 'Create table', 42 74 custom_css: 'Custom CSS', 75 daily: 'Daily', 43 76 dark: 'Dark', 44 77 date: 'Date', 78 deactivate: 'Deactivate', 79 descending: 'Descending', 80 desktop: 'Desktop', 45 81 delete: 'Delete', 46 82 delete_column: 'Delete column', 83 delete_group: 'Delete group', 47 84 delete_row: 'Delete row', 48 85 dense: 'Dense', 49 86 dense_mode: 'Dense mode', 87 dimensions: 'Dimensions', 88 dismiss: 'Dismiss', 50 89 done: 'Done', 90 configure_indexes: 'Configure indexed columns', 91 configure_columns: 'Configure columns', 92 create_without_indexes: 'Create without indexes', 93 index_column: 'Index', 94 data_type: 'Data type', 95 type_text: 'Text', 96 type_number: 'Number', 97 type_date: 'Date', 98 type_boolean: 'Boolean', 99 start_import: 'Start import', 100 skip: 'Skip', 51 101 download: 'Download', 52 dimensions: 'Dimensions',53 102 edit: 'Edit', 54 103 edit_table: 'Edit table', 104 enable_column_groups: 'Enable column groups', 105 every_1_minute: 'Every 1 minute', 106 every_5_minutes: 'Every 5 minutes', 107 every_15_minutes: 'Every 15 minutes', 108 every_30_minutes: 'Every 30 minutes', 109 every_30_seconds: 'Every 30 seconds', 110 every_hour: 'Every hour', 111 exp_date: 'Exp. date', 112 expire_soon: 'Expire soon', 113 expired: 'Expired', 55 114 export: 'Export', 115 external: 'External', 56 116 failed: 'Action failed', 57 117 fetch: 'Fetch', … … 59 119 filesize: 'File Size', 60 120 filled: 'Filled', 121 filter: 'Filter', 122 filter_column: 'Column', 123 filter_logic: 'Filter logic', 124 filter_operator: 'Operator', 125 filter_value: 'Value', 61 126 flat: 'Flat', 62 127 format: 'Format', … … 64 129 fullscreen_mode: 'Fullscreen mode (applies for Table view only)', 65 130 general: 'General', 131 get_license: 'Get License', 132 get_new_license: 'Get new license', 133 get_plan: 'Get Plan', 134 go_to_cart: 'Go to cart', 135 google_sheets_api_key: 'Google Sheets API Key', 66 136 grid: 'Grid', 67 137 grid_header: 'Grid header', 138 group_name: 'Group name', 139 group_tree: 'Group tree', 68 140 header: 'Header', 69 141 header_background_color: 'Header background', 70 142 header_css_class: 'Header CSS class', 143 header_groups: 'Header Groups', 71 144 header_text_color: 'Header text color', 145 hidden: 'Hidden', 72 146 hide: 'Hide', 73 147 hide_bottom: 'Hide bottom', 74 148 hide_header: 'Hide header', 75 149 hide_pagination: 'Hide pagination', 150 hide_settings: 'Hide settings', 76 151 hide_title: 'Hide title', 77 152 horizontal: 'Horizontal', 78 hyperlink_color: 'Hyperlink color', 79 hyperlink_text: 'Hyperlink text', 80 hyperlinks_type: 'Hyperlinks type', 153 i_want_to: 'I want to..', 81 154 image: 'Image', 155 image_height: 'Image height', 156 image_popup: 'Image popup', 157 image_width: 'Image width', 82 158 import: 'Import', 83 159 import_all_products: 'Import all products', 84 i_want_to: 'I want to', 160 import_products_from_categories: 'Import products from categories', 161 in_stock_label: 'In stock label', 162 initialize_from_columns: 'Initialize from columns', 163 json_preview_lines: 'JSON preview lines', 164 license: 'License', 165 lifetime: 'Lifetime', 85 166 link: 'Link', 167 logs: 'Logs', 168 migration_status: 'Migration status', 169 migration_completed: 'Completed', 170 migration_in_progress: 'In progress', 171 link_color: 'Link color', 172 link_text: 'Link text', 173 link_to_cart: 'Link to cart', 174 links_as: 'Links as', 86 175 local: 'Local', 87 176 local_csv_file: 'local CSV file', 88 177 merged_selected_cells: 'Merge selected cells', 178 learn_more: 'Learn more', 179 pro: 'Pro', 180 pro_teaser: { 181 external: 'Import tables from any CSV or JSON URL and keep them in sync automatically.', 182 google_sheets: 'Connect a Google Sheet and keep the table in sync automatically.', 183 woocommerce: 'Build product tables from your WooCommerce catalog, with add to cart.', 184 columns: 'Show or hide columns, pin the first or last column, and set column widths.', 185 column_groups: 'Group columns under shared multi-level headers.', 186 css: 'Add your own CSS and classes to style the table.', 187 filter: 'Filter rows by conditions, for visitors and in the admin.', 188 pagination: 'Choose how many rows each page shows.', 189 view: 'Show the table as responsive cards on desktop, tablet and mobile.', 190 rows: 'Image size, lightbox preview and vertical alignment for cells, plus video, PDF, audio, map, rating, progress and JSON cells.', 191 search: 'Add a search box so visitors can find rows.', 192 sorting: 'Let visitors sort the table by any column.', 193 }, 89 194 messages: { 90 available_in_pro_version: 'Available in PRO version', 91 available_in_pro_version_under_advanced_subscription_plan: 92 'Available in PRO version (Advanced subscription)', 93 available_in_pro_version_under_lite_subscription_plan: 94 'Available in PRO version (Lite subscription)', 195 available_in_pro: 'Available in vTables Pro.', 196 table_requires_pro: 'This table uses a vTables Pro data source or row filter. Its data is kept unchanged; it can be opened and shown with vTables Pro.', 197 add_filter_conditions_to_table: 'Filter table rows using custom conditions', 95 198 add_search_field_to_table: 'Add search field to table', 96 199 add_your_first_table: 'Add your first table', 200 adds_link_to_cart_in_the_notifications: 201 'Adds link to cart in the notifications', 97 202 all_selected_tables: 'all selected tables', 98 203 allows_showing_images_in_table_if_it_has_columns_with_images_paths: … … 100 205 allows_users_open_fullsize_image_in_popup: 101 206 'Allows user to open a full-size image in a popup', 207 and_set_it_in_the: 'and set it in the', 102 208 applies_a_default_border: 'Applies a default border to the component', 103 209 applies_dense_mode: 'Applies a dense mode (applies for Table view only)', … … 105 211 are_you_sure_you_want_to_reset_your_current_settings: 106 212 'Are you sure you want to reset current settings?', 213 available_for_external_google_sheets_woocommerce: 214 'Sync is available for WooCommerce, Google Sheets, and external URL tables only', 215 available_for_table_view_only: 'Available for Table view only', 216 available_for_woocommerce_tables_only: 217 'Available for WooCommerce tables only', 218 available_in: 'Available in', 219 available_with: 'Available with', 220 cant_extract_sheet_id: "Can't extract sheet ID", 221 categories_not_found: 'Categories not found', 222 category_doesnt_exists_or_empty: "Category doesn't exist or empty", 223 changes_have_been_applied: 'Changes have been applied', 107 224 changes_have_been_saved: 'Changes have been saved', 225 column_groups_summary: 226 '{groups} groups · {depth} levels deep · {assigned} of {total} columns assigned', 108 227 column_mismatch: 'Column mismatch.', 228 confirm_clear_group_tree: 229 'This will permanently remove the entire group hierarchy. This cannot be undone.', 230 confirm_remove_license: 231 'This will remove the license key from this site. You can add it again later.', 109 232 couldnt_parse_file_content: "Couldn't parse file content", 233 custom_css_scoped_to_this_table: 234 "CSS is automatically scoped to this table. Write plain selectors, e.g. .q-table th {'{'} background: #eee; {'}'}", 110 235 css_classes_to_apply_to_card: 111 236 'CSS classes to apply to the card (when in grid mode)', … … 115 240 "CSS classes to apply to native HTML table element's wrapper (DIV)", 116 241 css_classes_to_apply_to_title: 'CSS classes to apply to the title', 242 current_data_will_be_overwritten: 243 'Current data will be overwritten by data from external source (Google Sheets)', 117 244 data_has_been_synced: 'The data has been synced', 118 245 display_data_as_table_or_grid: 'Display data as a table or grid', 119 drag_and_drop_csv_file_to_upload: 'Drag and Drop a CSV file(s) to upload', 246 dont_have_a_key_yet_get_one_from_the: 247 "Don't have a key yet? Get one from the", 248 drag_and_drop_csv_file_to_upload: 249 'Drag and Drop a CSV, JSON or Excel file(s) to upload', 250 drag_to_reorder: 'Drag to reorder', 251 drop_column_here: 'Drop a column here', 252 external_source_column_name_too_long: 'This source has a column name longer than {max} characters — it does not look like a data file.', 253 external_source_is_not_tabular: 'This URL returns a web page, not a data file. Link directly to a CSV or JSON file.', 254 external_source_too_many_columns: 'This source has {count} columns (limit {max}) — it does not look like a data file.', 255 file_too_large: 'File is too large', 256 file_size_limit_is_50mb: 'Maximum file size is 50 MB', 257 file_size_limit_is_10mb: 'Maximum file size is 10 MB', 258 table_source_unrenderable: 'This table has {count} columns and cannot be displayed. It was most likely imported from a web page instead of a data file — delete it and re-import from a direct CSV or JSON link.', 259 uploading_large_file: 'Uploading large file…', 260 choose_up_to_5_columns_to_index: 261 'Choose up to 5 columns to index for faster sorting, filtering and search. Pick the data type for each so comparisons behave correctly.', 262 enable_column_groups_hint: 263 'Group columns under shared header labels with arbitrary nesting depth.', 264 enable_virtual_scroll: 265 'Enable virtual scrolling to efficiently render large datasets. A fixed table height must be set in the Dimensions tab.', 266 external_source_not_found: 'External source not found', 120 267 field_is_required: 'Field is required', 268 unable_to_configure_indexes: 269 'The table was created, but the indexed columns could not be configured.', 270 must_be_whole_number_between: 271 'Must be a whole number between {min} and {max}', 121 272 file_not_found_or_not_accessible: 'File not found or not accessible', 273 fetched_rows_so_far: 'Fetched {n} rows so far…', 122 274 for_first_sticky_column: 'for first sticky column', 123 275 for_last_sticky_column: 'for last sticky column', 124 276 for_sticky_columns_make_sure_you_set_table_width: 125 'For sticky columns make sure you set table width on "Dimensions" tab .',277 'For sticky columns make sure you set table width on "Dimensions" tab if your table does not have a horizontal scrollbar.', 126 278 for_sticky_header_make_sure_you_set_table_height: 127 'For sticky header make sure you set table height on "Dimensions" tab .',279 'For sticky header make sure you set table height on "Dimensions" tab if your table does not have a vertical scrollbar.', 128 280 fullscreen_mode_available_only_for_table_view: 129 281 'Fullscreen mode available only for Table view', 282 google_api_key_has_been_removed: 'Google Sheets API key has been removed', 283 google_api_key_has_been_saved: 'Google Sheets API key has been saved', 284 google_api_key_is_invalid: 285 'This Google Sheets API key could not be validated', 286 google_api_key_problem_banner: 287 'There is a problem with your Google Sheets API key. Tables connected to Google Sheets may stop syncing.', 288 google_sheets_api_key_hint: 289 'When set, Google Sheets imports, resyncs, and the scheduled auto-sync use the official Google Sheets API instead of the public CSV endpoint.', 290 google_sheets_api_key_invalid_notice: 291 'Your Google Sheets API key is invalid or restricted, so data is fetched through a public endpoint instead — this is not reliable and may return incomplete data.', 292 google_sheets_no_api_key_notice: 293 'Importing and syncing Google Sheets without an API key is not reliable — data is fetched through a public endpoint and may be incomplete or fail to load.', 130 294 how_many_columns_from_left_will_be_sticky: 131 295 'How many columns from left will be sticky', 296 migration_log_hint: 297 'Events recorded while tables created by older plugin versions are moved to the new row storage in the background.', 298 no_migration_events_yet: 'No migration events yet.', 132 299 how_many_rows_from_top_will_be_sticky: 133 300 'How many rows from the top will be sticky', 301 license_has_been_activated: 'License has been activated', 302 license_has_been_deactivated: 'License has been deactivated', 303 license_has_been_removed: 'License has been removed', 304 layout_on_desktop_screens: 'Layout on desktop screens (≥ 1440px)', 305 layout_on_mobile_screens: 'Layout on mobile screens (< 600px)', 306 layout_on_tablet_screens: 'Layout on tablet screens (600px – 1439px)', 134 307 make_first_column_sticky: 'Make first column "sticky"', 135 308 make_header_sticky: 'Make header "sticky"', 136 309 make_last_column_sticky: 'Make last column "sticky"', 310 message_has_been_sent_thank_you: 'Message has been sent! Thank you!', 137 311 minimum_number_of_rows_to_render: 138 312 'Minimum number of rows to render in the virtual list', 313 multiple_upload_available_with_premium_plans: 314 'Multiple upload available with Premium plans', 139 315 only_letters_numbers_dashes_and_underscores_allowed: 140 316 'Only letters, numbers, dashes and underscores allowed', 141 317 paste_a_google_sheet_url: 'Paste a google sheet URL', 318 please_check_the_category: 'Please check the category', 319 please_deactivate_license_before_using_it_on_different_site: 320 'Please deactivate license before using it on different site', 142 321 please_ensure_you_only_use_alphanumeric_characters_dashes_underscores_spaces_and_parantheses_for_filenames: 143 322 'Please ensure you only use alphanumeric characters, dashes, underscores, spaces, and parentheses for filenames.', 323 please_get_and_activate_new_license: 324 'Please get and activate a new license', 325 please_get_license: 'Please get a license', 326 please_make_sure_cart_link_set_in_woocommerce_settings: 327 'Please make sure cart link set in WooCommerce settings', 144 328 please_make_sure_the_columns_align_: 145 329 'Please make sure the columns in new spreadsheet align with the table columns.', 146 330 please_make_sure_the_file_is_valid: 'Please make sure the file is valid', 331 please_make_sure_woocommerce_categories_exist: 332 'Please make sure WooCommerce categories exist', 333 please_make_sure_woocommerce_is_intalled_and_activated: 334 'Please make sure woocommerce is installed and activated', 147 335 please_set_table_height: 'Please set table height', 336 please_try_again_later: 'Please try again later', 337 please_turn_on_enable_ajax_option_in_the_woo_settings: 338 'Please select "Enable AJAX add to cart buttons on archives" option in the WooCommerce settings for a better user experience.', 148 339 please_use_unique_value: 'Please use unique value', 149 340 ratio_of_number_of_rows_after: … … 151 342 ratio_of_number_of_rows_before: 152 343 'Ratio of number of rows in visible zone to render before it', 344 redirect_to_the_cart_page_after_successful_addition: 345 'Redirect to the cart page after successful addition', 346 review_your_key_in_the: 'Review your key in the', 153 347 select_separators: 'Select separators', 348 set_add_to_cart_button_label: 'Set add-to-cart button label', 349 set_auto_sync_interval: 350 'How often the table data is automatically refreshed from the external source', 351 manual_sync_tooltip: 352 'Immediately update the table with fresh data from its source. Available for WooCommerce, Google Sheets, and external URL tables.', 353 use_google_sheets_tab_for_this_url: 'This is a Google Sheets link — import it from the “Google Sheets” tab.', 354 woocommerce_auto_sync_is_realtime: 355 'WooCommerce tables update in real time as products change; the daily task runs a full index reconciliation', 154 356 set_background_color: 'Set background color', 155 357 set_background_color_for_bottom_section: … … 164 366 set_button_text: 'Set button text', 165 367 set_button_text_color: 'Set button text color', 368 set_cart_link_text: 'Set link text', 166 369 set_cell_vertical_alignment: 167 370 'Set cell vertical alignment (applies for Table view only)', 168 371 set_column_sort_order: 'Set column sort order', 169 372 set_columns_max_width: 'Set columns max width', 170 set_hyperlink_color: 'Set text color', 171 set_hyperlink_text: 'Set hyperlink text', 373 set_columns_min_width: 'Set columns min width', 374 set_in_stock_label: 'Set in_stock label', 375 set_json_preview_lines: 376 'Set the number of lines shown in JSON cell preview before truncating', 377 set_link_color: 'Set text color', 378 set_link_text: 'Set link text', 379 set_notification_background_color: 'Set notification background color', 380 set_notification_heading: 'Set notification heading', 381 set_notification_message: 'Set notification message', 382 set_notification_text_color: 'Set notification text color', 383 set_notification_timer: 384 'Set notification timer in milliseconds, set -1 to make a notification permanent', 385 set_out_of_stock_label: 'Set out_of_stock label', 172 386 set_pagination_label: 'Set rows per page label', 173 387 set_pagination_options: 'Set pagination options', 388 set_path_to_open_cart: 'Set path to cart in notifications', 174 389 set_records_per_page: 'Set records per page', 390 set_background_color_for_search_field: 391 'Set background color for search field', 175 392 set_search_field_color: 'Set search field color', 176 393 set_search_input_style: 'Set search input style', 394 set_style_of_stock_badge: 'Set style of stock badge', 177 395 set_table_height: 'Set table height (applies for Table view only)', 178 396 set_table_max_width: 'Set table max width (applies for Table view only)', … … 187 405 settings_have_been_reset: 'Settings have been reset', 188 406 settings_have_been_saved: 'Settings have been saved', 189 show_hyperlinks_in_table_if_it_has_columns_with_hyperlinks: 190 'Show hyperlinks as links/buttons in table if it has column/s with hyperlinks', 407 show_links_in_table_if_it_has_columns_with_links: 408 'Show links as links/buttons in table if it has column/s with links', 409 shows_notification_when_item_added_to_cart: 410 'Shows notification when item added to cart', 191 411 size_in_px_of_sticky_header: 192 412 'Size in pixels of the sticky header (if using one); A correct value will improve scroll precision.', … … 194 414 'Skip the third state (unsorted) when user toggles column sort direction', 195 415 something_went_wrong: 'Something went wrong..', 196 please_try_again_later: 'Please try again later', 416 start_by_initializing_tree: 417 'Start by initializing the tree from your existing columns, or add a group manually.', 418 table_type_not_available_under_your_plan: 419 "{tableType} tables aren't available under your {plan} plan.", 197 420 table_with_name_already_exists: 'Table with name already exists', 198 421 this_is_supported_only_on_modern_browsers: 199 422 'This is supported only on modern browsers', 423 this_setting_applies_to_table_view_only: 424 'This setting applies to Table view only and has no effect when the table is displayed in Grid mode.', 425 to_add_product_to_cart_please_use_shortcode_on_frontpage: 426 'To add product to cart please use shortcode on front page', 427 trial_ended_sync_disabled: 428 'Your trial has ended. Upgrade to continue syncing.', 429 trial_table_limit_reached: 430 'Trial allows only 1 table. Delete your existing table to create a different one, or upgrade.', 200 431 turn_on_off_sorting: 'Turn ON/OFF sorting for columns', 201 432 unable_to_cancel_changes: 'Unable to cancel changes.', … … 203 434 unable_to_reset_settings: 'Unable to reset settings', 204 435 unable_to_save_changes: 'Unable to save changes.', 436 unsaved_changes: 'Unsaved changes', 437 unsaved_changes_will_be_lost: 438 'You have unsaved table changes. If you leave this page, they will be lost.', 205 439 use_separator_between_rows_cols_cells: 206 440 'Use a separator between rows, columns or all cells', 207 441 use_to_display_large_number_of_rows: 208 442 'Use to display large number of rows (for non-grid mode only)', 209 message_has_been_sent_thank_you: 'Message has been sent! Thank you!', 210 file_has_been_fetched: 'File has been fetched', 211 external_source_not_found: 'External source not found', 212 cant_extract_sheet_id: "Can't extract sheet ID", 213 license_has_been_activated: 'License has been activated', 214 license_has_been_deactivated: 'License has been deactivated', 215 please_deactivate_license_before_using_it_on_different_site: 216 'Please deactivate license before using it on different site', 217 available_for_table_view_only: 'Available for Table view only', 443 virtual_scroll_slice_ratio_after: 444 'Ratio of extra rows pre-rendered below the visible area. Higher values reduce blank rows when scrolling down.', 445 virtual_scroll_slice_ratio_before: 446 'Ratio of extra rows pre-rendered above the visible area. Higher values reduce blank rows when scrolling up.', 447 virtual_scroll_slice_size: 448 'Number of rows rendered at a time. Increase for smoother scrolling, decrease to improve performance on large datasets.', 449 virtual_scroll_sticky_size_start: 450 'Height in pixels reserved for sticky rows pinned to the top of the virtual list.', 451 woocommerce_cart_link_not_found: 'WooCommerce cart link not found', 452 woocommerce_not_found: 'WooCommerce not found', 453 wraps_product_name_into_a_link: 'Wraps product name into a link', 218 454 your_license_expire_soon: 'Your license expire soon', 219 your_license_is_expired: 'Your license is expired', 220 please_get_and_activate_new_license: 221 'Please get and activate a new license', 455 your_license_is_expired: 'Your license has expired', 456 your_plan_is_expired: 'Your {plan} plan is expired.', 222 457 }, 223 valid: 'Valid',224 expired: 'Expired',225 expire_soon: 'Expire soon',226 license: 'License',227 exp_date: 'Exp. date',228 458 middle: 'Middle', 459 mobile: 'Mobile', 229 460 modified: 'Modified', 461 more: 'More', 462 new_group: 'New group', 230 463 none: 'None', 464 not_connected: 'Not connected', 231 465 nothing_found: 'Nothing found', 466 notification_background_color: 'Notification bg color', 467 notification_heading: 'Notification heading', 468 notification_message: 'Notification message', 469 notification_text_color: 'Notification text color', 470 notification_timer: 'Notification timer', 471 notifications: 'Notifications', 472 number_of_columns: 'Columns', 473 number_of_rows: 'Rows', 232 474 off: 'Off', 233 475 on: 'On', 234 476 open: 'Open', 235 or: 'Or', 477 open_in_google_sheets: 'Open in Google Sheets', 478 operator_contains: 'Contains', 479 operator_ends_with: 'Ends with', 480 operator_equals: 'Equals', 481 operator_greater_than: 'Greater than', 482 operator_is_empty: 'Is empty', 483 operator_is_not_empty: 'Is not empty', 484 operator_less_than: 'Less than', 485 operator_not_contains: 'Does not contain', 486 operator_not_equals: 'Does not equal', 487 operator_starts_with: 'Starts with', 488 or: 'or', 489 out_of_stock_label: 'Out of stock label', 236 490 outlined: 'Outlined', 491 page: 'Page', 237 492 pagination: 'Pagination', 238 493 pagination_label: 'Pagination label', 239 494 pagination_options: 'Pagination options', 495 path_to_cart: 'Path to cart', 240 496 pick_file: 'Pick File', 497 plan: 'Plan', 498 plans: 'plans', 241 499 preview_mode: 'Preview mode', 500 product_link: 'Product link', 501 products: 'Products', 242 502 records_per_age: 'Records per page', 243 503 remote: 'Remote', 504 refresh: 'Refresh', 244 505 remove: 'Remove', 506 remove_license: 'Remove license', 245 507 remove_selected: 'Remove selected', 246 508 remove_top_row: 'Remove top row', 247 509 remove_uploaded_files: 'Remove Uploaded Files', 510 rename_column: 'Rename column', 511 rename_group: 'Rename group', 512 renew: 'Renew', 248 513 reset: 'Reset', 514 reset_done: 'Reset!', 515 responsive: 'Responsive', 249 516 rows: 'Rows', 250 517 save: 'Save', … … 253 520 search_field_color: 'Search field color', 254 521 search_input_style: 'Search input style', 522 select_categories: 'Select categories', 255 523 select_color: 'Select color', 524 select_option: 'Select option', 256 525 separator: 'Separator', 257 526 settings: 'Settings', … … 261 530 show_hide_columns: 'Show/Hide columns', 262 531 show_images: 'Show images', 532 show_more: 'Show more', 533 show_notifications: 'Show notifications', 534 show_settings: 'Show settings', 263 535 sort_order: 'Sort order', 264 536 sorting: 'Sorting', … … 267 539 square: 'Square', 268 540 standard: 'Standard', 541 status: 'Status', 269 542 sticky_first_column: 'Sticky 1st col.', 270 543 sticky_header: 'Sticky header', 271 544 sticky_last_column: 'Sticky last col.', 545 stock_badge: 'Stock badge', 546 style: 'Style', 272 547 success: 'Action was successful', 273 548 sync: 'Sync', 274 549 table: 'table', 550 table_as: 'table as', 275 551 table_css_class: 'Table CSS class', 276 552 table_height: 'Table height', 277 table_width: 'Table width',278 553 table_max_width: 'Table max width', 279 554 table_name: 'Table name', 555 table_width: 'Table width', 280 556 tables: 'Tables', 557 tablet: 'Tablet', 281 558 text_color: 'Text color', 282 thumbnail_height: 'Thumbnail height',283 thumbnail_width: 'Thumbnail width',284 559 title: 'Title', 285 560 title_css_class: 'Title CSS class', 286 561 top: 'Top', 562 twice_daily: 'Twice a day', 287 563 turn_links_into_buttons: 'Turn links into buttons', 288 564 type: 'Type', 565 unassigned_columns: 'Unassigned columns', 566 upgrade: 'Upgrade', 289 567 upload: 'Upload', 290 568 upload_files: 'Upload Files', 291 569 upload_more: 'Upload More', 292 570 uploaded: 'Uploaded', 571 valid: 'Valid', 572 invalid: 'Invalid', 293 573 vertical: 'Vertical', 294 574 vertical_alignment: 'Vertical alignment', -
vtables/trunk/src/i18n/index.ts
r3576926 r3754379 1 1 import enUS from './en-US'; 2 import es from './es'; 3 import de from './de'; 4 import fr from './fr'; 5 import hi from './hi'; 6 import it from './it'; 7 import nb from './nb'; 8 import pt from './pt'; 9 import ru from './ru'; 10 import sv from './sv'; 11 import tr from './tr'; 12 import zhCN from './zh-CN'; 13 import zhHK from './zh-HK'; 2 14 3 15 export default { 4 'en-US': enUS 16 'en-US': enUS, 17 es: es, 18 de: de, 19 fr: fr, 20 hi: hi, 21 it: it, 22 nb: nb, 23 pt: pt, 24 ru: ru, 25 sv: sv, 26 tr: tr, 27 'zh-CN': zhCN, 28 'zh-HK': zhHK, 5 29 }; -
vtables/trunk/uninstall.php
r3036822 r3754379 1 1 <?php 2 if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) exit; 3 4 /** 5 * UNINSTALL 6 * 7 * Removes this plugin's options, scheduled events and upload working files. 8 * The tables themselves (wp_vtables, wp_vtables_source) are shared with 9 * vTables Pro and hold the user's content, so they are only dropped on 10 * explicit request — add to wp-config.php before deleting the plugin: 11 * 12 * define( 'VTABLES_REMOVE_ALL_DATA', true ); 13 * 14 * and even then never while vTables Pro is still installed. 15 */ 16 17 function vtables_uninstall_site() { 18 global $wpdb; 19 20 foreach ( array( 21 'vtables_db_version', 22 'vtables_rewrite_version', 23 'vtables_rows_migrated', 24 'vtables_rows_migration_skipped', 25 'vtables_migration_log', 26 ) as $option ) { 27 delete_option( $option ); 28 } 29 30 wp_clear_scheduled_hook( 'vtables_migrate_rows_tick' ); 31 32 // Everything below is shared with vTables Pro (same names), so it stays 33 // while Pro is installed. 34 if ( file_exists( WP_PLUGIN_DIR . '/vtables-pro/vtables-pro.php' ) ) { 35 return; 36 } 37 38 // Per-table count-cache versions and their transients, plus import sessions. 39 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 40 "DELETE FROM {$wpdb->options} 41 WHERE option_name LIKE 'vtables\\_count\\_ver\\_%' 42 OR option_name LIKE '\\_transient\\_vtables\\_count\\_%' 43 OR option_name LIKE '\\_transient\\_timeout\\_vtables\\_count\\_%' 44 OR option_name LIKE '\\_transient\\_vtables\\_import\\_%' 45 OR option_name LIKE '\\_transient\\_timeout\\_vtables\\_import\\_%'" 46 ); 47 48 if ( defined( 'VTABLES_REMOVE_ALL_DATA' ) && VTABLES_REMOVE_ALL_DATA ) { 49 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}vtables_source" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange 50 $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}vtables" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.SchemaChange 51 } 52 } 53 54 function vtables_uninstall_remove_dir( $dir ) { 55 if ( ! is_dir( $dir ) ) { 56 return; 57 } 58 59 foreach ( array_diff( (array) scandir( $dir ), array( '.', '..' ) ) as $file ) { 60 $path = $dir . $file; 61 if ( is_dir( $path ) ) { 62 vtables_uninstall_remove_dir( $path . '/' ); 63 } else { 64 wp_delete_file( $path ); 65 } 66 } 67 68 rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir 69 } 70 71 if ( is_multisite() ) { 72 foreach ( get_sites( array( 'fields' => 'ids' ) ) as $vtables_site_id ) { 73 switch_to_blog( $vtables_site_id ); 74 vtables_uninstall_site(); 75 restore_current_blog(); 76 } 77 } else { 78 vtables_uninstall_site(); 79 } 80 81 // Upload working files only (chunks/assembled imports in progress) — never 82 // the user's media. Left alone while vTables Pro, which shares it, is installed. 83 if ( ! file_exists( WP_PLUGIN_DIR . '/vtables-pro/vtables-pro.php' ) ) { 84 $vtables_upload_info = wp_upload_dir(); 85 vtables_uninstall_remove_dir( trailingslashit( $vtables_upload_info['basedir'] ) . 'vtables_uploads/' ); 86 } -
vtables/trunk/vtables.php
r3576926 r3754379 2 2 3 3 /** 4 * Plugin Name: vTables 5 * Description: Create stunning, interactive tables effortlessly. No coding required. The ultimate WordPress plugin, lets you design powerful tables in a few clicks. Transform your data visualization now. 6 * Author: vTables 7 * Author URI: https://vtables.pro 8 * Version: 0.2.1 9 * License: GPLv3 4 * Plugin Name: vTables – Data Tables from CSV, Excel and JSON 5 * Plugin URI: https://vtables.pro 6 * Description: Build interactive data tables from CSV, Excel and JSON files, or from scratch, and embed them anywhere with a shortcode. 7 * Version: 0.3 8 * Requires at least: 6.0 9 * Requires PHP: 7.4 10 * Author: vTables 11 * Author URI: https://vtables.pro 12 * License: GPLv3 13 * License URI: https://www.gnu.org/licenses/gpl-3.0.html 14 * Text Domain: vtables 10 15 */ 11 16 12 17 if ( ! defined( 'ABSPATH' ) ) exit; 13 18 14 define('VTABLES_URL', plugin_dir_url(__FILE__)); 15 define('VTABLES_DIR', plugin_dir_path(__FILE__)); 16 define('VTABLES_VERSION', '0.2.1'); 19 define( 'VTABLES_VERSION', '0.3' ); 20 define( 'VTABLES_FILE', __FILE__ ); 21 define( 'VTABLES_DIR', plugin_dir_path( __FILE__ ) ); 22 define( 'VTABLES_URL', plugin_dir_url( __FILE__ ) ); 23 24 require_once VTABLES_DIR . 'includes/compat.php'; 17 25 18 26 class vTables { 19 27 public function vtables_boot() { 20 $this->vtables_add_module_to_script();21 28 $this->vtables_load_classes(); 22 $this->vtables_add_upgrade_link(); 29 $this->vtables_settings_policy(); 30 $this->vtables_maybe_upgrade(); 23 31 $this->vtables_register_shortcodes(); 24 $this->vtables_ activate_plugin();32 $this->vtables_embed(); 25 33 $this->vtables_render_menu(); 34 $this->vtables_action_links(); 26 35 $this->vtables_db_endpoints(); 27 } 28 29 /** 30 * ADD MODULE TO SCRIPT HOOK 31 */ 32 public function vtables_add_module_to_script() { 33 add_filter('script_loader_tag', array($this, 'vtables_add_module_to_script_handler'), 10, 3); 34 } 35 36 /** 37 * ADD MODULE TO SCRIPT HANDLER 38 */ 39 function vtables_add_module_to_script_handler($tag, $handle, $src) { 40 if ($handle === 'vtables-script-boot') { 41 $tag = str_replace('<script ', '<script type="module" id="vtables-script-boot" ', $tag); 42 } 43 return $tag; 36 $this->vtables_migrate_rows(); 44 37 } 45 38 … … 48 41 */ 49 42 public function vtables_load_classes() { 50 require VTABLES_DIR . 'includes/autoload.php'; 51 } 52 53 /** 54 * ADD LINK TO UPGRADE HOOK 55 */ 56 public function vtables_add_upgrade_link() { 57 add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), array($this, 'vtables_upgrade_link_handler') ); 58 } 59 60 /** 61 * ADD LINK TO UPGRADE HANDLER 62 */ 63 function vtables_upgrade_link_handler( array $links ) { 64 $settings_link = '<b><a target="_blank" href="https://vtables.pro">' . __('Download Pro Add-on for free', 'vtables') . '</a></b>'; 65 $links[] = $settings_link; 66 return $links; 43 require_once VTABLES_DIR . 'includes/autoload.php'; 44 45 // SimpleXLSX / SimpleXLS for spreadsheet imports. 46 if ( file_exists( VTABLES_DIR . 'vendor/php/autoload.php' ) ) { 47 require_once VTABLES_DIR . 'vendor/php/autoload.php'; 48 } 49 } 50 51 /** 52 * SETTINGS POLICY 53 * Loaded before the endpoints: save_settings/create_table filter every 54 * settings blob through it. 55 */ 56 public function vtables_settings_policy() { 57 include_once VTABLES_DIR . 'includes/settings-policy.php'; 67 58 } 68 59 … … 71 62 */ 72 63 public function vtables_render_menu() { 73 add_action( 'admin_menu', function () {74 if ( !current_user_can('manage_options')) {64 add_action( 'admin_menu', function () { 65 if ( ! current_user_can( 'manage_options' ) ) { 75 66 return; 76 67 } 77 global $submenu; 68 78 69 add_menu_page( 79 70 'vTables', … … 81 72 'manage_options', 82 73 'vtables', 83 array( $this, 'vtables_render_admin_page'),84 plugins_url( 'public/icons/vtables-menu-icon-color.png', __FILE__),74 array( $this, 'vtables_render_admin_page' ), 75 plugins_url( 'public/icons/vtables-menu-icon-color.png', __FILE__ ), 85 76 25 86 77 ); 87 }); 78 79 global $submenu; 80 $submenu['vtables']['tables'] = array( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 81 __( 'Tables', 'vtables' ), 82 'manage_options', 83 'admin.php?page=vtables#/', 84 ); 85 86 // The Logs page only reports the legacy row migration, so it appears 87 // once there is something to report. 88 if ( ! empty( get_option( 'vtables_migration_log' ) ) ) { 89 $submenu['vtables']['logs'] = array( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 90 __( 'Logs', 'vtables' ), 91 'manage_options', 92 'admin.php?page=vtables#/logs', 93 ); 94 } 95 } ); 88 96 } 89 97 … … 93 101 public function vtables_render_admin_page() { 94 102 $loadAssets = new \vTables\Classes\LoadAssets(); 95 $loadAssets->vtables_enqueue_assets(); 96 97 $WP_API = apply_filters('vTables/admin_app_vars', array( 98 'assets_url' => VTABLES_URL . 'dist/assets/', 99 'ajaxurl' => admin_url('admin-ajax.php'), 100 'nonce' => wp_create_nonce( 'wp_rest' ), 101 'base_url' => rest_url(), 102 )); 103 104 wp_localize_script('vtables-script-boot', 'WP_API_Settings', $WP_API); 105 103 $loadAssets->vtables_enqueue_assets_admin(); 106 104 echo '<div id="q-app"></div>'; 107 105 } 108 106 109 107 /** 108 * PLUGIN ACTION LINKS 109 * One "Upgrade" link on the Plugins screen, only while Pro isn't installed. 110 */ 111 public function vtables_action_links() { 112 add_filter( 'plugin_action_links_' . plugin_basename( VTABLES_FILE ), function ( $links ) { 113 if ( vtables_is_pro_installed() ) { 114 return $links; 115 } 116 117 $links[] = '<a href="' . esc_url( 'https://vtables.pro' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Upgrade to Pro', 'vtables' ) . '</a>'; 118 return $links; 119 } ); 120 } 121 122 /** 110 123 * REGISTER SHORTCODES 111 124 */ 112 125 public function vtables_register_shortcodes() { 113 global $shortcode_count; 114 $shortcode_count = 0; 115 116 // render shortcode 117 function vtables_shortcode_render_handler($atts = []) { 118 global $wpdb; 119 global $shortcode_count; 120 $id = isset($atts['id']) ? intval($atts['id']) : 0; 121 122 // render specific cell value 123 $row_number = isset($atts['row']) ? intval($atts['row']) : 0; 124 $col_name = isset($atts['column']) ? sanitize_text_field($atts['column']) : ''; 125 126 if ($row_number > 0 && !empty($col_name)) { 127 $col_name_snakecase = strtolower(str_replace(' ', '_', $col_name)); 128 $table_name = $wpdb->prefix . 'vtables'; 129 $query = $wpdb->prepare("SELECT * FROM $table_name WHERE id = %d", $id); 130 $results = $wpdb->get_results($query); 131 132 if (empty($results)) { 133 return 'Value not found'; 134 } 135 136 $source = json_decode($results[0]->source); 137 $rows = $source->rows; 138 $row_index = $row_number - 1; 139 140 if (!isset($rows[$row_index])) { 141 return 'Row not found.'; 142 } 143 144 $cell_value = isset($rows[$row_index]->{$col_name_snakecase}) ? $rows[$row_index]->{$col_name_snakecase} : ''; 145 146 return $cell_value; 147 } 148 149 150 // render table 151 $shortcode_count++; 152 153 ob_start(); 154 155 ?> 156 157 <div class="vtables-iframe-wrapper"> 158 <iframe 159 name="<?php echo esc_attr($shortcode_count); ?>" 160 data-id="<?php echo esc_attr($id); ?>" 161 data-count="<?php echo esc_attr($shortcode_count); ?>" 162 data-baseurl="<?php echo esc_url(rest_url()); ?>" 163 src="<?php echo esc_url(VTABLES_URL . 'includes/shortcode.html'); ?>" 164 style="width: 100%; height: 150px; border: 0 none;" 165 ></iframe> 166 <div> 167 168 169 <?php 170 171 return ob_get_clean(); 172 } 173 add_shortcode( 'vtables', 'vtables_shortcode_render_handler' ); 174 } 175 176 /** 177 * ACTIVATE PLUGIN 178 */ 179 public function vtables_activate_plugin() { 180 register_activation_hook(__FILE__, function ($newWorkWide) { 181 require_once(VTABLES_DIR . 'includes/Classes/Activator.php'); 182 $activator = new \vTables\Classes\Activator(); 183 $activator->vtables_migrate_databases($newWorkWide); 184 }); 126 add_shortcode( 'vtables', array( $this, 'vtables_shortcode_render_handler' ) ); 127 } 128 129 /** 130 * EMBED DOCUMENT 131 * Serves the [vtables] iframe document from WordPress (/vtables-embed/{id}/), 132 * on the same URL scheme as vTables Pro. See includes/Classes/Embed.php. 133 */ 134 public function vtables_embed() { 135 ( new \vTables\Classes\Embed() )->register(); 136 } 137 138 /** 139 * RENDER SPECIFIC VALUE 140 */ 141 public function vtables_render_specific_value( $row_number, $col_name, $id ) { 142 global $wpdb; 143 144 $col_name_snakecase = strtolower( str_replace( ' ', '_', $col_name ) ); 145 $row_index = $row_number - 1; 146 147 $table = vtables_table_meta( $id ); 148 149 if ( $table === null ) { 150 return __( 'Value not found', 'vtables' ); 151 } 152 153 // Pro-only sources, and tables whose saved row filter only Pro applies. 154 if ( vtables_table_requires_pro( $table ) ) { 155 return ''; 156 } 157 158 // Rows live in wp_vtables_source (one record per row, keyed by table_id + 159 // row_index). Tables that still keep their rows inline (0.2.x and older, 160 // not yet migrated) fall through to wp_vtables.source->rows below. 161 $has_migrated_rows = (int) $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 162 "SELECT COUNT(*) FROM {$wpdb->prefix}vtables_source WHERE table_id = %d", $id 163 ) ) > 0; 164 165 if ( $has_migrated_rows ) { 166 $row_json = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 167 "SELECT row_json FROM {$wpdb->prefix}vtables_source WHERE table_id = %d AND row_index = %d", 168 $id, 169 $row_index 170 ) ); 171 172 if ( $row_json === null ) { 173 return __( 'Row not found.', 'vtables' ); 174 } 175 176 $row = json_decode( $row_json ); 177 return ( is_object( $row ) && isset( $row->{$col_name_snakecase} ) ) ? $row->{$col_name_snakecase} : ''; 178 } 179 180 $legacy_source = $wpdb->get_var( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 181 "SELECT source FROM {$wpdb->prefix}vtables WHERE id = %d", $id 182 ) ); 183 184 $source = json_decode( (string) $legacy_source ); 185 $rows = ( is_object( $source ) && isset( $source->rows ) ) ? $source->rows : array(); 186 187 if ( ! isset( $rows[ $row_index ] ) ) { 188 return __( 'Row not found.', 'vtables' ); 189 } 190 191 return isset( $rows[ $row_index ]->{$col_name_snakecase} ) ? $rows[ $row_index ]->{$col_name_snakecase} : ''; 192 } 193 194 /** 195 * RENDER SHORTCODES 196 */ 197 public function vtables_shortcode_render_handler( $atts = array() ) { 198 $atts = is_array( $atts ) ? $atts : array(); 199 $id = isset( $atts['id'] ) ? intval( $atts['id'] ) : 0; 200 201 // render specific cell value 202 $row_number = isset( $atts['row'] ) ? intval( $atts['row'] ) : 0; 203 $col_name = isset( $atts['column'] ) ? sanitize_text_field( $atts['column'] ) : ''; 204 if ( $row_number > 0 && ! empty( $col_name ) ) { 205 $value = $this->vtables_render_specific_value( $row_number, $col_name, $id ); 206 207 // json-type cells decode to arrays/objects. 208 if ( ! is_scalar( $value ) ) { 209 $value = wp_json_encode( $value ); 210 } 211 212 // Cell data is untrusted, so escape on output. Allowlist mirrors the 213 // SPA's sanitizeCellHtml (src/composables/useCellRendering.ts) to keep 214 // both render paths in parity. 215 return wp_kses( (string) $value, array( 216 'a' => array( 'href' => true, 'title' => true, 'target' => true, 'rel' => true, 'class' => true ), 217 'img' => array( 'src' => true, 'alt' => true, 'title' => true, 'class' => true, 'width' => true, 'height' => true ), 218 'br' => array(), 219 'button' => array( 'href' => true, 'title' => true, 'target' => true, 'rel' => true, 'class' => true ), 220 ) ); 221 } 222 223 // Tables from Pro-only sources (Google Sheets, external URLs, WooCommerce) 224 // or with a saved row filter render nothing for visitors; admins get a 225 // pointer instead of a blank spot. 226 if ( vtables_table_requires_pro( vtables_table_meta( $id ) ) ) { 227 if ( ! current_user_can( 'manage_options' ) ) { 228 return ''; 229 } 230 231 return '<p class="vtables-requires-pro">' . esc_html( 232 /* translators: %d: table ID. */ 233 sprintf( __( 'vTables table #%d needs vTables Pro (Pro data source or row filter), so it is hidden from visitors.', 'vtables' ), $id ) 234 ) . '</p>'; 235 } 236 237 $iframe_url = \vTables\Classes\Embed::url( $id ); 238 239 ob_start(); 240 ?> 241 242 <div class="vtables-iframe-wrapper"> 243 <iframe 244 src="<?php echo esc_url( $iframe_url ); ?>" 245 title="<?php echo esc_attr__( 'vTables table', 'vtables' ); ?>" 246 loading="lazy" 247 referrerpolicy="same-origin" 248 style="width: 100%; height: 150px; border: 0 none;" 249 ></iframe> 250 </div> 251 252 <?php 253 254 return ob_get_clean(); 255 } 256 257 /** 258 * VERSION-GATED UPGRADE 259 * register_activation_hook does not fire when a plugin is updated in place, 260 * and 0.2.x never stored a schema version, so the migrations run on the 261 * first request after an update — gated on the 'vtables_db_version' option, 262 * which makes this a single autoloaded get_option() in the common case. 263 */ 264 public function vtables_maybe_upgrade() { 265 if ( get_option( 'vtables_db_version' ) === VTABLES_VERSION ) { 266 return; 267 } 268 269 $activator = new \vTables\Classes\Activator(); 270 $activator->vtables_migrate_databases( false ); 271 272 update_option( 'vtables_db_version', VTABLES_VERSION ); 185 273 } 186 274 … … 189 277 */ 190 278 public function vtables_db_endpoints() { 191 register_rest_field( 192 'user', 193 'user_email', 194 [ 195 'get_callback' => static function (array $user): string { 196 return get_userdata($user['id'])->user_email; 197 }, 198 ] 199 ); 200 201 include VTABLES_DIR . 'includes/endpoints.php'; 202 } 203 279 include_once VTABLES_DIR . 'includes/endpoints.php'; 280 } 281 282 /** 283 * LEGACY ROW MIGRATION (WP-CRON) 284 * Moves 0.2.x inline source->rows into wp_vtables_source in the background. 285 * Included after db_endpoints() — the worker persists through 286 * vtables_replace_source_rows() and friends defined there. 287 */ 288 public function vtables_migrate_rows() { 289 include_once VTABLES_DIR . 'includes/migrate-rows.php'; 290 } 204 291 } 205 292 206 if ( 207 in_array( 'vtables-pro/vtables-pro.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) || 208 in_array( 'vtables.pro/vtables-pro.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) 209 ) { 210 // Do nothing as Pro version installed and activated. 211 } else { 212 (new vTables())->vtables_boot(); 293 /** 294 * ACTIVATE PLUGIN 295 * Registered at file level: WordPress fires the activation hook on a request 296 * where plugins_loaded has already run. 297 */ 298 register_activation_hook( __FILE__, 'vtables_activate' ); 299 function vtables_activate( $network_wide = false ) { 300 require_once VTABLES_DIR . 'includes/autoload.php'; 301 $activator = new \vTables\Classes\Activator(); 302 $activator->vtables_migrate_databases( $network_wide ); 303 304 // 'init' already fired for this request, so the embed rule has to be 305 // registered here or the flush would rebuild the rewrite array without it. 306 \vTables\Classes\Embed::register_rewrite(); 307 flush_rewrite_rules( false ); 213 308 } 214 309 215 216 217 218 310 /** 311 * DEACTIVATE PLUGIN 312 */ 313 register_deactivation_hook( __FILE__, 'vtables_deactivate' ); 314 function vtables_deactivate() { 315 wp_clear_scheduled_hook( 'vtables_migrate_rows_tick' ); 316 delete_option( 'vtables_rewrite_version' ); 317 flush_rewrite_rules( false ); 318 } 319 320 /** 321 * BOOT 322 * Deferred to plugins_loaded so every plugin has been included first: when 323 * vTables Pro is running it owns the admin screen, the shortcode, the embed 324 * document and the endpoints (on the same tables and URLs), and this plugin 325 * stays dormant. 326 */ 327 add_action( 'plugins_loaded', 'vtables_bootstrap' ); 328 function vtables_bootstrap() { 329 if ( vtables_is_pro_running() ) { 330 return; 331 } 332 333 ( new vTables() )->vtables_boot(); 334 }
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)