Changeset 3361722
- Timestamp:
- 09/15/2025 11:34:52 AM (7 months ago)
- Location:
- integromat-connector
- Files:
-
- 44 added
- 24 edited
-
tags/1.6.0 (added)
-
tags/1.6.0/api (added)
-
tags/1.6.0/api/authentication.php (added)
-
tags/1.6.0/api/response.php (added)
-
tags/1.6.0/assets (added)
-
tags/1.6.0/assets/integromat-white.svg (added)
-
tags/1.6.0/assets/iwc.css (added)
-
tags/1.6.0/assets/iwc.js (added)
-
tags/1.6.0/class (added)
-
tags/1.6.0/class/class-api-permissions.php (added)
-
tags/1.6.0/class/class-api-token.php (added)
-
tags/1.6.0/class/class-file-validator.php (added)
-
tags/1.6.0/class/class-guard.php (added)
-
tags/1.6.0/class/class-logger.php (added)
-
tags/1.6.0/class/class-rate-limiter.php (added)
-
tags/1.6.0/class/class-rest-request.php (added)
-
tags/1.6.0/class/class-rest-response.php (added)
-
tags/1.6.0/class/class-user.php (added)
-
tags/1.6.0/index.php (added)
-
tags/1.6.0/licence.txt (added)
-
tags/1.6.0/readme.txt (added)
-
tags/1.6.0/settings (added)
-
tags/1.6.0/settings/class-controller.php (added)
-
tags/1.6.0/settings/class-meta-object.php (added)
-
tags/1.6.0/settings/events.php (added)
-
tags/1.6.0/settings/object-types (added)
-
tags/1.6.0/settings/object-types/class-comments-meta.php (added)
-
tags/1.6.0/settings/object-types/class-post-meta.php (added)
-
tags/1.6.0/settings/object-types/class-term-meta.php (added)
-
tags/1.6.0/settings/object-types/class-user-meta.php (added)
-
tags/1.6.0/settings/object-types/custom-taxonomy.php (added)
-
tags/1.6.0/settings/object-types/general.php (added)
-
tags/1.6.0/settings/object-types/security.php (added)
-
tags/1.6.0/settings/render.php (added)
-
tags/1.6.0/settings/template (added)
-
tags/1.6.0/settings/template/customFields.phtml (added)
-
tags/1.6.0/settings/template/custom_taxonomies.phtml (added)
-
tags/1.6.0/settings/template/general_menu.phtml (added)
-
tags/1.6.0/settings/template/security_settings.phtml (added)
-
trunk/api/authentication.php (modified) (3 diffs)
-
trunk/assets/iwc.css (modified) (4 diffs)
-
trunk/assets/iwc.js (modified) (1 diff)
-
trunk/class/class-api-permissions.php (added)
-
trunk/class/class-api-token.php (modified) (3 diffs)
-
trunk/class/class-file-validator.php (added)
-
trunk/class/class-guard.php (modified) (2 diffs)
-
trunk/class/class-logger.php (modified) (7 diffs)
-
trunk/class/class-rate-limiter.php (added)
-
trunk/class/class-rest-request.php (modified) (4 diffs)
-
trunk/class/class-rest-response.php (modified) (2 diffs)
-
trunk/class/class-user.php (modified) (4 diffs)
-
trunk/index.php (modified) (6 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/settings/class-controller.php (modified) (4 diffs)
-
trunk/settings/class-meta-object.php (modified) (2 diffs)
-
trunk/settings/events.php (modified) (1 diff)
-
trunk/settings/object-types/class-comments-meta.php (modified) (4 diffs)
-
trunk/settings/object-types/class-post-meta.php (modified) (5 diffs)
-
trunk/settings/object-types/class-term-meta.php (modified) (4 diffs)
-
trunk/settings/object-types/class-user-meta.php (modified) (4 diffs)
-
trunk/settings/object-types/custom-taxonomy.php (modified) (3 diffs)
-
trunk/settings/object-types/general.php (modified) (4 diffs)
-
trunk/settings/object-types/security.php (added)
-
trunk/settings/render.php (modified) (3 diffs)
-
trunk/settings/template/customFields.phtml (modified) (1 diff)
-
trunk/settings/template/custom_taxonomies.phtml (modified) (1 diff)
-
trunk/settings/template/general_menu.phtml (modified) (1 diff)
-
trunk/settings/template/security_settings.phtml (added)
Legend:
- Unmodified
- Added
- Removed
-
integromat-connector/trunk/api/authentication.php
r2784613 r3361722 26 26 27 27 if ( $skip ) { 28 $log && \Integromat\Logger::write( implode( ' ,', $codes ) );28 $log && \Integromat\Logger::write( implode( ';', $codes ) ); 29 29 return $result; 30 30 } … … 32 32 if ( isset( $_SERVER['HTTP_IWC_API_KEY'] ) && ! empty( $_SERVER['HTTP_IWC_API_KEY'] ) ) { 33 33 34 $token = sanitize_text_field( $_SERVER['HTTP_IWC_API_KEY']);34 $token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_IWC_API_KEY'] ) ); 35 35 36 36 if ( strlen( $token ) !== \Integromat\Api_Token::API_TOKEN_LENGTH || ! \Integromat\Api_Token::is_valid( $token ) ) { … … 38 38 \Integromat\Rest_Response::render_error( 401, 'Provided API key is invalid', 'invalid_token' ); 39 39 } else { 40 \Integromat\User::login( $user_id ); 40 // Check rate limiting 41 $rate_limit_id = \Integromat\Rate_Limiter::get_identifier(); 42 if ( \Integromat\Rate_Limiter::is_rate_limited( $rate_limit_id ) ) { 43 $rate_status = \Integromat\Rate_Limiter::get_rate_limit_status( $rate_limit_id ); 44 $log && \Integromat\Logger::write( 9 ); 45 \Integromat\Rest_Response::render_error( 46 429, 47 'Rate limit exceeded. Try again later.', 48 'rate_limit_exceeded', 49 array( 50 'X-RateLimit-Limit' => $rate_status['limit'], 51 'X-RateLimit-Remaining' => max( 0, $rate_status['limit'] - $rate_status['requests'] ), 52 'X-RateLimit-Reset' => $rate_status['reset_time'], 53 ) 54 ); 55 } 56 57 // Check payload size 58 if ( \Integromat\Rate_Limiter::is_payload_too_large() ) { 59 $log && \Integromat\Logger::write( 10 ); 60 \Integromat\Rest_Response::render_error( 413, 'Request payload too large', 'payload_too_large' ); 61 } 62 63 // Extract endpoint and method for permission checking 64 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; 65 $method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET'; 66 67 $endpoint = ''; 68 if ( preg_match( '#\/wp-json/(.*?)(\?.*)?$#i', $request_uri, $matches ) ) { 69 $endpoint = '/' . $matches[1]; 70 } 71 72 // Use safer user context setting with permission checking 73 if ( ! \Integromat\User::set_api_user_context( $user_id, $endpoint, $method ) ) { 74 $log && \Integromat\Logger::write( 8 ); 75 \Integromat\Rest_Response::render_error( 403, 'Insufficient API permissions', 'insufficient_permissions' ); 76 } 41 77 $log && \Integromat\Logger::write( 7 ); 42 78 \Integromat\Rest_Request::dispatch(); -
integromat-connector/trunk/assets/iwc.css
r2529734 r3361722 16 16 background-color: white; 17 17 padding: 30px; 18 margin-right: 20px; 18 19 } 19 20 #imt-content-panel section { … … 42 43 43 44 .imapie_settings_container.wait { 44 filter: opacity(20%) 45 filter: opacity(20%); 45 46 } 46 47 47 48 .imapie_settings_container.wait * { 48 49 cursor: wait; 50 } 51 52 .nav-tab-wrapper { 53 border-bottom: 1px solid #ccd0d4; 54 padding-left: 10px; 55 position: relative; 56 z-index: 10; 57 background: #fff; 58 overflow: hidden; 59 /* Ensures tabs don't wrap unexpectedly */ 60 } 61 62 .nav-tab { 63 background: #f1f1f1; 64 border: 1px solid #ccd0d4; 65 border-bottom: none; 66 color: #555; 67 display: inline-block; 68 font-size: 14px; 69 line-height: 1.71428571; 70 margin: 0 5px -1px 0; 71 padding: 8px 12px; 72 text-decoration: none; 73 white-space: nowrap; 74 cursor: pointer; 75 transition: all 0.2s ease; 76 position: relative; 77 vertical-align: top; 78 } 79 80 .nav-tab:hover { 81 background-color: #fff; 82 color: #444; 83 text-decoration: none; 84 border-color: #999; 85 } 86 87 .nav-tab-active, 88 .nav-tab-active:hover { 89 background-color: #fff; 90 border-bottom: 1px solid #fff; 91 color: #000; 92 font-weight: 600; 93 position: relative; 94 z-index: 11; 95 border-color: #ccd0d4; 96 } 97 98 .iwc-tab-content { 99 display: none; 100 padding: 15px 0 0; 101 position: relative; 102 z-index: 1; 103 } 104 105 .iwc-tab-content.iwc-tab-active { 106 display: block; 107 animation: fadeIn 0.3s ease-in-out; 108 } 109 110 @keyframes fadeIn { 111 from { opacity: 0; } 112 to { opacity: 1; } 113 } 114 115 /* Ensure consistent styling between General Settings and Custom Fields */ 116 .imapie_settings_container { 117 max-width: none; 118 } 119 120 .imapie_settings_container .form-table { 121 margin-top: 20px; 122 } 123 124 .imapie_settings_container .form-table th { 125 width: 200px; 126 min-width: 200px; 127 } 128 129 .imapie_settings_container .form-table td { 130 padding: 20px 0; 131 } 132 133 .imapie_settings_container h3 { 134 color: #23282d; 135 font-size: 18px; 136 font-weight: 600; 137 margin: 30px 0 10px; 138 padding: 0; 139 } 140 141 .imapie_settings_container p.desc, 142 .imapie_settings_container .description { 143 color: #666; 144 font-style: italic; 145 margin: 5px 0 0; 146 } 147 148 .imapie_settings_container p.submit { 149 margin: 20px 0 0; 150 padding: 0; 151 } 152 153 /* Consistent form element styling */ 154 .imapie_settings_container select, 155 .imapie_settings_container input[type="text"], 156 .imapie_settings_container input[type="number"], 157 .imapie_settings_container input[type="email"], 158 .imapie_settings_container textarea { 159 border: 1px solid #ddd; 160 border-radius: 3px; 161 padding: 6px 8px; 162 font-size: 14px; 163 line-height: 1.4; 164 background-color: #fff; 165 color: #32373c; 166 box-shadow: inset 0 1px 2px rgba(0,0,0,.07); 167 transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 168 } 169 170 .imapie_settings_container select:focus, 171 .imapie_settings_container input[type="text"]:focus, 172 .imapie_settings_container input[type="number"]:focus, 173 .imapie_settings_container input[type="email"]:focus, 174 .imapie_settings_container textarea:focus { 175 border-color: #0073aa; 176 box-shadow: 0 0 0 1px #0073aa; 177 outline: none; 178 } 179 180 .imapie_settings_container input[type="checkbox"] { 181 margin: 0 4px 0 0; 182 vertical-align: middle; 183 } 184 185 .imapie_settings_container label { 186 vertical-align: middle; 187 cursor: pointer; 188 } 189 190 /* API Permissions specific styling to match General Settings */ 191 .iwc-permissions-grid { 192 display: grid; 193 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 194 gap: 15px; 195 border: 1px solid #ddd; 196 padding: 15px; 197 background: #f9f9f9; 198 border-radius: 3px; 199 margin-top: 10px; 200 } 201 202 .iwc-permission-group h4 { 203 margin: 0 0 8px 0; 204 font-size: 13px; 205 font-weight: 600; 206 color: #23282d; 207 } 208 209 .iwc-permission-group label { 210 display: block; 211 margin-bottom: 4px; 212 font-size: 12px; 213 color: #555; 214 } 215 216 .iwc-permission-group input[type="checkbox"] { 217 margin-right: 5px; 218 } 219 220 /* Buttons consistency */ 221 .imapie_settings_container .button { 222 border: 1px solid #0073aa; 223 border-radius: 3px; 224 text-decoration: none; 225 text-shadow: none; 226 font-size: 13px; 227 line-height: 2.15384615; 228 min-height: 30px; 229 margin: 0; 230 padding: 0 10px; 231 cursor: pointer; 232 } 233 234 /* Notices styling for consistency */ 235 .imapie_settings_container .notice { 236 margin: 15px 0; 237 padding: 10px; 238 border-radius: 3px; 239 border-left: 4px solid; 240 } 241 242 .imapie_settings_container .notice-info { 243 background: #e7f3ff; 244 border-left-color: #0073aa; 245 color: #0c4a6e; 246 } 247 248 .imapie_settings_container .notice-warning { 249 background: #fff3cd; 250 border-left-color: #ffb900; 251 color: #8b4513; 252 } 253 254 .imapie_settings_container .notice-error { 255 background: #f8d7da; 256 border-left-color: #d63638; 257 color: #721c24; 258 } 259 260 .imapie_settings_container .notice-success { 261 background: #d4edda; 262 border-left-color: #46b450; 263 color: #155724; 264 } 265 266 /* Accessibility and Form Validation Improvements */ 267 .error { 268 border-color: #d63638 !important; 269 box-shadow: 0 0 2px rgba(214, 54, 56, 0.5); 270 } 271 272 .error:focus { 273 border-color: #d63638 !important; 274 outline: 2px solid #d63638; 275 outline-offset: 1px; 276 } 277 278 /* Focus states for better accessibility */ 279 input[type="checkbox"]:focus, 280 input[type="text"]:focus, 281 select:focus, 282 textarea:focus { 283 outline: 2px solid #0073aa; 284 outline-offset: 1px; 285 } 286 287 /* High contrast mode support */ 288 @media (prefers-contrast: high) { 289 .uncheck_all { 290 text-decoration: underline; 291 font-weight: bold; 292 } 293 294 .error { 295 border-width: 2px; 296 } 297 } 298 299 /* API Key Management Styles */ 300 .iwc-api-key-container { 301 display: flex; 302 align-items: center; 303 gap: 10px; 304 margin-bottom: 10px; 305 } 306 307 /* API Key revealed state styling */ 308 #iwc-api-key-value[data-state="revealed"] { 309 background-color: #fff3cd; 310 border-color: #ffeaa7; 311 box-shadow: 0 0 0 1px #ffeaa7; 312 } 313 314 /* Countdown indicator for auto-hide */ 315 .iwc-countdown { 316 font-size: 12px; 317 color: #666; 318 margin-left: 10px; 319 font-style: italic; 320 } 321 322 /* Log Actions Styles */ 323 .iwc-log-actions { 324 display: flex; 325 align-items: center; 326 gap: 10px; 327 margin-bottom: 10px; 328 } 329 330 /* Modal Styles for Regenerate Confirmation */ 331 .iwc-modal { 332 display: none; 333 position: fixed; 334 z-index: 100000; 335 left: 0; 336 top: 0; 337 width: 100%; 338 height: 100%; 339 background-color: rgba(0, 0, 0, 0.5); 340 backdrop-filter: blur(2px); 341 } 342 343 .iwc-modal-content { 344 background-color: #fff; 345 margin: 5% auto; 346 padding: 0; 347 border: 1px solid #ddd; 348 border-radius: 4px; 349 width: 90%; 350 max-width: 600px; 351 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 352 animation: iwc-modal-appear 0.3s ease-out; 353 } 354 355 @keyframes iwc-modal-appear { 356 from { 357 opacity: 0; 358 transform: translateY(-50px); 359 } 360 to { 361 opacity: 1; 362 transform: translateY(0); 363 } 364 } 365 366 .iwc-modal-header { 367 background-color: #fcf2f2; 368 padding: 15px 20px; 369 border-bottom: 1px solid #ddd; 370 display: flex; 371 justify-content: space-between; 372 align-items: center; 373 border-radius: 4px 4px 0 0; 374 } 375 376 .iwc-modal-header h3 { 377 margin: 0; 378 font-size: 18px; 379 color: #d63638; 380 display: flex; 381 align-items: center; 382 gap: 8px; 383 } 384 385 .iwc-modal-header h3:before { 386 content: "⚠️"; 387 font-size: 20px; 388 } 389 390 .iwc-modal-close { 391 font-size: 24px; 392 font-weight: bold; 393 color: #999; 394 cursor: pointer; 395 line-height: 1; 396 transition: color 0.2s ease; 397 } 398 399 .iwc-modal-close:hover { 400 color: #333; 401 } 402 403 .iwc-modal-body { 404 padding: 20px; 405 } 406 407 .iwc-warning-box { 408 background-color: #fff3cd; 409 border: 1px solid #ffeaa7; 410 border-radius: 4px; 411 padding: 15px; 412 margin-bottom: 20px; 413 } 414 415 .iwc-warning-box strong { 416 color: #856404; 417 } 418 419 .iwc-form-group { 420 margin-bottom: 15px; 421 } 422 423 .iwc-form-group label { 424 display: block; 425 margin-bottom: 5px; 426 font-weight: bold; 427 color: #333; 428 } 429 430 .iwc-checkbox-group { 431 display: flex; 432 align-items: flex-start; 433 gap: 8px; 434 margin-bottom: 15px; 435 } 436 437 .iwc-checkbox-group input[type="checkbox"] { 438 margin-top: 3px; 439 } 440 441 .iwc-checkbox-group label { 442 margin-bottom: 0; 443 font-weight: normal; 444 cursor: pointer; 445 } 446 447 .iwc-form-group input[type="text"] { 448 width: 100%; 449 padding: 8px 12px; 450 border: 1px solid #ddd; 451 border-radius: 3px; 452 font-size: 14px; 453 transition: border-color 0.2s ease; 454 } 455 456 .iwc-form-group input[type="text"]:focus { 457 outline: none; 458 border-color: #0073aa; 459 box-shadow: 0 0 0 1px #0073aa; 460 } 461 462 .iwc-form-actions { 463 margin-top: 20px; 464 display: flex; 465 gap: 10px; 466 justify-content: flex-end; 467 padding-top: 15px; 468 border-top: 1px solid #ddd; 469 } 470 471 .iwc-form-actions .button { 472 min-width: 100px; 473 } 474 475 .iwc-confirm-btn { 476 border-color: #d63638 !important; 477 color: #d63638 !important; 478 } 479 480 .iwc-confirm-btn:hover:not(:disabled) { 481 background-color: #d63638; 482 color: white !important; 483 } 484 485 .iwc-confirm-btn:disabled { 486 opacity: 0.5; 487 cursor: not-allowed; 488 } 489 490 /* Responsive design */ 491 @media (max-width: 600px) { 492 .iwc-api-key-container { 493 flex-direction: column; 494 align-items: flex-start; 495 gap: 8px; 496 } 497 498 .iwc-api-key-container input { 499 width: 100%; 500 margin-bottom: 8px; 501 } 502 503 .iwc-modal-content { 504 margin: 2% auto; 505 width: 95%; 506 } 507 508 .iwc-form-actions { 509 flex-direction: column-reverse; 510 } 511 512 .iwc-form-actions .button { 513 width: 100%; 514 min-width: auto; 515 } 516 517 /* Tab responsive design */ 518 .nav-tab-wrapper { 519 display: flex; 520 flex-wrap: wrap; 521 gap: 2px; 522 margin-bottom: 15px; 523 } 524 525 .nav-tab { 526 flex: 1; 527 text-align: center; 528 min-width: 80px; 529 font-size: 12px; 530 padding: 6px 8px; 531 } 532 533 #imt-content-panel { 534 padding: 15px; 535 margin-right: 10px; 536 } 537 538 .imapie_settings_container .form-table th { 539 width: auto; 540 min-width: auto; 541 display: block; 542 padding-bottom: 5px; 543 } 544 545 .imapie_settings_container .form-table td { 546 display: block; 547 padding: 5px 0 15px; 548 } 549 550 .iwc-permissions-grid { 551 grid-template-columns: 1fr; 552 gap: 10px; 553 padding: 10px; 554 } 555 } 556 557 @media (max-width: 782px) { 558 .nav-tab { 559 font-size: 12px; 560 padding: 6px 10px; 561 } 562 563 .integromat_api_row th { 564 min-width: auto; 565 width: auto; 566 } 567 } 568 569 /* Success/Error Messages */ 570 .iwc-message { 571 padding: 10px 15px; 572 margin: 15px 0; 573 border-radius: 4px; 574 font-weight: bold; 575 } 576 577 .iwc-message.success { 578 background-color: #d4edda; 579 border: 1px solid #c3e6cb; 580 color: #155724; 581 } 582 583 .iwc-message.error { 584 background-color: #f8d7da; 585 border: 1px solid #f5c6cb; 586 color: #721c24; 587 } 588 589 /* Tab-specific messages that appear after Save Settings button */ 590 .iwc-tab-message { 591 margin: 15px 0 0; 592 padding: 10px 12px; 593 border-left: 4px solid; 594 background: #fff; 595 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); 596 } 597 598 .iwc-tab-message.notice-success { 599 border-left-color: #46b450; 600 background: #d4edda; 601 } 602 603 /* Simple messages for non-tabbed forms like Custom Taxonomies */ 604 .iwc-simple-message { 605 margin: 15px 0 0; 606 padding: 10px 12px; 607 border-left: 4px solid; 608 background: #fff; 609 box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); 610 border-radius: 0 3px 3px 0; 611 } 612 613 .iwc-simple-message.notice-success { 614 border-left-color: #46b450; 615 background: #d4edda; 616 color: #155724; 617 } 618 619 .iwc-simple-message.notice-error { 620 border-left-color: #d63638; 621 background: #f8d7da; 622 color: #721c24; 623 } 624 625 .iwc-simple-message.notice-warning { 626 border-left-color: #ffb900; 627 background: #fff3cd; 628 color: #8b4513; 629 } 630 631 .iwc-simple-message.notice-info { 632 border-left-color: #0073aa; 633 background: #e7f3ff; 634 color: #0c4a6e; 635 } 636 637 .iwc-tab-message.notice-error { 638 border-left-color: #d63638; 639 background: #f8d7da; 640 } 641 642 .iwc-tab-message p { 643 margin: 0; 644 font-weight: 600; 49 645 } 50 646 … … 52 648 cursor: pointer; 53 649 text-decoration: underline; 54 }55 56 .imapie_settings_container .ui-tabs {57 position: relative;58 padding: .2em;59 }60 61 .imapie_settings_container .ui-tabs .ui-tabs-nav {62 margin: 0;63 padding: .2em .2em 0;64 }65 66 .imapie_settings_container .ui-tabs .ui-tabs-nav li {67 list-style: none;68 float: left;69 position: relative;70 top: 0;71 margin: 1px .2em 0 0;72 border-bottom-width: 0;73 padding: 0;74 white-space: nowrap;75 background-color: white;76 }77 78 .imapie_settings_container .ui-tabs .ui-tabs-nav .ui-tabs-anchor {79 float: left;80 padding: .5em 1em;81 text-decoration: none;82 }83 84 .imapie_settings_container .ui-tabs .ui-tabs-nav li.ui-tabs-active {85 margin-bottom: -1px;86 padding-bottom: 1px;87 }88 89 .imapie_settings_container .ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,90 .imapie_settings_container .ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,91 .imapie_settings_container .ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor {92 cursor: text;93 }94 95 .imapie_settings_container .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor {96 cursor: pointer;97 }98 99 .imapie_settings_container .ui-tabs .ui-tabs-panel {100 display: block;101 border-width: 0;102 padding: 1em 1.4em;103 background: none;104 }105 106 /* Component containers107 ----------------------------------*/108 .imapie_settings_container .ui-widget.ui-widget-content {109 border: 1px solid #c5c5c5;110 }111 112 .imapie_settings_container .ui-widget-content {113 border: 1px solid #dddddd;114 background: #ffffff;115 color: #333333;116 }117 118 .imapie_settings_container .ui-widget-content a {119 color: #333333;120 }121 122 .imapie_settings_container .ui-widget-header {123 border: 1px solid #dddddd;124 background: #e9e9e9;125 color: #333333;126 font-weight: bold;127 }128 129 .imapie_settings_container .ui-widget-header a {130 color: #333333;131 }132 133 /* Layout helpers134 ----------------------------------*/135 .imapie_settings_container .ui-helper-clearfix:before,136 .imapie_settings_container .ui-helper-clearfix:after {137 content: "";138 display: table;139 border-collapse: collapse;140 }141 142 .imapie_settings_container .ui-helper-clearfix:after {143 clear: both;144 }145 146 .imapie_settings_container .ui-helper-zfix {147 width: 100%;148 height: 100%;149 top: 0;150 left: 0;151 position: absolute;152 opacity: 0;153 filter: Alpha(Opacity=0);154 }155 156 .imapie_settings_container .ui-front {157 z-index: 100;158 }159 .imapie_settings_container .iwc-comment {160 font-style: italic;161 color: #a8a8a8;162 650 } 163 651 … … 172 660 } 173 661 174 .imapie_settings_container .w-200 { 175 width: 200px 176 } 662 .imapie_settings_container .iwc-comment { 663 font-style: italic; 664 color: #a8a8a8; 665 } -
integromat-connector/trunk/assets/iwc.js
r2777334 r3361722 3 3 $(document).ready(function () { 4 4 5 // API Key reveal/hide functionality 6 let apiKeyTimeout; 7 let countdownInterval; 8 9 $('#iwc-api-key-toggle').on('click', function() { 10 const $button = $(this); 11 const $input = $('#iwc-api-key-value'); 12 const state = $button.data('state'); 13 14 if (state === 'masked') { 15 // Fetch the key dynamically via AJAX 16 revealApiKey($button, $input); 17 } else { 18 // Hide the key 19 hideApiKey($button, $input); 20 } 21 }); 22 23 // API Key regenerate functionality 24 $('#iwc-api-key-regenerate').on('click', function() { 25 showRegenerateModal(); 26 }); 27 28 function showRegenerateModal() { 29 // Create modal HTML 30 const modalHtml = ` 31 <div id="iwc-regenerate-modal" class="iwc-modal"> 32 <div class="iwc-modal-content"> 33 <div class="iwc-modal-header"> 34 <h3>Regenerate API Key</h3> 35 <span class="iwc-modal-close">×</span> 36 </div> 37 <div class="iwc-modal-body"> 38 <div class="iwc-warning-box"> 39 <strong>⚠️ WARNING:</strong> Regenerating the API key will immediately break ALL existing connections between your WordPress site and Make that use this key. 40 </div> 41 42 <p><strong>This action is irreversible.</strong> You will need to:</p> 43 <ul> 44 <li>Update all your connections with the new API key on Make</li> 45 <li>Test all connections after regeneration</li> 46 </ul> 47 48 <div class="iwc-checkbox-group"> 49 <input type="checkbox" id="iwc-confirm-understand" required> 50 <label for="iwc-confirm-understand"> 51 I understand that this will break all existing connections and this action cannot be undone. 52 </label> 53 </div> 54 55 <div class="iwc-form-group"> 56 <label for="iwc-confirm-text"> 57 Type <strong>"regenerate"</strong> to confirm: 58 </label> 59 <input type="text" id="iwc-confirm-text" placeholder="regenerate" autocomplete="off"> 60 </div> 61 62 <div class="iwc-form-actions"> 63 <button type="button" class="button iwc-modal-cancel">Cancel</button> 64 <button type="button" id="iwc-confirm-regenerate" class="button iwc-confirm-btn" disabled> 65 Regenerate API Key 66 </button> 67 </div> 68 </div> 69 </div> 70 </div> 71 `; 72 73 // Remove existing modal if any 74 $('#iwc-regenerate-modal').remove(); 75 76 // Add modal to body 77 $('body').append(modalHtml); 78 79 // Show modal 80 $('#iwc-regenerate-modal').show(); 81 82 // Focus on checkbox 83 $('#iwc-confirm-understand').focus(); 84 85 // Enable/disable confirm button based on validation 86 function validateForm() { 87 const isChecked = $('#iwc-confirm-understand').is(':checked'); 88 const textMatch = $('#iwc-confirm-text').val().toLowerCase() === 'regenerate'; 89 $('#iwc-confirm-regenerate').prop('disabled', !(isChecked && textMatch)); 90 } 91 92 $('#iwc-confirm-understand, #iwc-confirm-text').on('change keyup', validateForm); 93 94 // Handle confirm button 95 $('#iwc-confirm-regenerate').on('click', function() { 96 const $button = $(this); 97 const originalText = $button.text(); 98 99 // Show loading state 100 $button.text('Regenerating...').prop('disabled', true); 101 102 // Make AJAX request 103 $.post(iwc_ajax.ajax_url, { 104 action: 'iwc_regenerate_api_key', 105 confirmation: $('#iwc-confirm-text').val(), 106 nonce: iwc_ajax.regenerate_nonce 107 }) 108 .done(function(response) { 109 if (response.success) { 110 // Update the API key field 111 const $input = $('#iwc-api-key-value'); 112 const $toggleBtn = $('#iwc-api-key-toggle'); 113 114 // Update data attributes 115 $input.data('masked', response.data.masked_token); 116 117 // Reset to masked state 118 hideApiKey($toggleBtn, $input); 119 $input.val(response.data.masked_token); 120 121 // Close modal 122 $('#iwc-regenerate-modal').remove(); 123 124 // Show success message 125 showMessage('API key regenerated successfully! Please update your Make.com connections with the new key.', 'success'); 126 } else { 127 showMessage('Error: ' + (response.data || 'Unknown error occurred'), 'error'); 128 } 129 }) 130 .fail(function() { 131 showMessage('Failed to regenerate API key. Please try again.', 'error'); 132 }) 133 .always(function() { 134 $button.text(originalText).prop('disabled', false); 135 }); 136 }); 137 138 // Handle modal close 139 $('.iwc-modal-close, .iwc-modal-cancel').on('click', function() { 140 $('#iwc-regenerate-modal').remove(); 141 }); 142 143 // Close modal on outside click 144 $('#iwc-regenerate-modal').on('click', function(e) { 145 if (e.target === this) { 146 $(this).remove(); 147 } 148 }); 149 } 150 151 // Log purge functionality 152 $('#iwc-log-purge').on('click', function() { 153 showPurgeModal(); 154 }); 155 156 function showPurgeModal() { 157 // Create modal HTML 158 const modalHtml = ` 159 <div id="iwc-purge-modal" class="iwc-modal"> 160 <div class="iwc-modal-content"> 161 <div class="iwc-modal-header"> 162 <h3>Purge Log Data</h3> 163 <span class="iwc-modal-close">×</span> 164 </div> 165 <div class="iwc-modal-body"> 166 <div class="iwc-warning-box"> 167 <strong>⚠️ WARNING:</strong> This will permanently delete all stored log data. 168 </div> 169 170 <p><strong>This action cannot be undone.</strong> All diagnostic and debug information will be lost.</p> 171 172 <p>Are you sure you want to purge all log data?</p> 173 174 <div class="iwc-form-actions"> 175 <button type="button" class="button iwc-modal-cancel">Cancel</button> 176 <button type="button" id="iwc-confirm-purge" class="button iwc-confirm-btn"> 177 Purge 178 </button> 179 </div> 180 </div> 181 </div> 182 </div> 183 `; 184 185 // Remove existing modal if any 186 $('#iwc-purge-modal').remove(); 187 188 // Add modal to body 189 $('body').append(modalHtml); 190 191 // Show modal 192 $('#iwc-purge-modal').show(); 193 194 // Handle confirm button 195 $('#iwc-confirm-purge').on('click', function() { 196 const $button = $(this); 197 const originalText = $button.text(); 198 199 // Show loading state 200 $button.text('Purging...').prop('disabled', true); 201 202 // Make AJAX request 203 $.post(iwc_ajax.ajax_url, { 204 action: 'iwc_purge_logs', 205 nonce: iwc_ajax.purge_nonce 206 }) 207 .done(function(response) { 208 // Close modal 209 $('#iwc-purge-modal').remove(); 210 211 if (response.success) { 212 // Show success message 213 showLogMessage('Log data purged successfully.', 'success'); 214 215 // Disable purge and download buttons since no logs exist 216 $('#iwc-log-purge, .iwc-log-actions a').addClass('disabled').prop('disabled', true); 217 } else { 218 showLogMessage('Error: ' + (response.data || 'Unknown error occurred'), 'error'); 219 } 220 }) 221 .fail(function() { 222 $('#iwc-purge-modal').remove(); 223 showLogMessage('Failed to purge log data. Please try again.', 'error'); 224 }) 225 .always(function() { 226 $button.text(originalText).prop('disabled', false); 227 }); 228 }); 229 230 // Handle modal close 231 $('.iwc-modal-close, .iwc-modal-cancel').on('click', function() { 232 $('#iwc-purge-modal').remove(); 233 }); 234 235 // Close modal on outside click 236 $('#iwc-purge-modal').on('click', function(e) { 237 if (e.target === this) { 238 $(this).remove(); 239 } 240 }); 241 } 242 243 function showLogMessage(text, type) { 244 // Remove existing messages 245 $('.iwc-log-message').remove(); 246 247 // Create new message 248 const $message = $('<div class="iwc-message iwc-log-message ' + type + '">' + text + '</div>'); 249 250 // Insert after the log actions container 251 $('.iwc-log-actions').after($message); 252 253 // Auto-remove after 5 seconds for success messages 254 if (type === 'success') { 255 setTimeout(function() { 256 $message.fadeOut(500, function() { 257 $(this).remove(); 258 }); 259 }, 5000); 260 } 261 } 262 263 function showMessage(text, type) { 264 // Remove existing messages 265 $('.iwc-message').remove(); 266 267 // Create new message 268 const $message = $('<div class="iwc-message ' + type + '">' + text + '</div>'); 269 270 // Insert after the API key container 271 $('.iwc-api-key-container').after($message); 272 273 // Auto-remove after 5 seconds for success messages 274 if (type === 'success') { 275 setTimeout(function() { 276 $message.fadeOut(500, function() { 277 $(this).remove(); 278 }); 279 }, 5000); 280 } 281 } 282 283 function showTabMessage(text, type, $form) { 284 // Remove existing tab messages 285 $('.iwc-tab-message').remove(); 286 287 // Create new message with WordPress native notice styling 288 const $message = $('<div class="notice notice-' + type + ' is-dismissible iwc-tab-message"><p>' + text + '</p></div>'); 289 290 // Insert after the submit button in the current form 291 $form.find('.button').after($message); 292 293 // Auto-remove after 5 seconds for success messages 294 if (type === 'success') { 295 setTimeout(function() { 296 $message.fadeOut(500, function() { 297 $(this).remove(); 298 }); 299 }, 5000); 300 } 301 } 302 303 function showSimpleMessage(text, type, $form) { 304 // Remove existing simple messages 305 $('.iwc-simple-message').remove(); 306 307 // Create new message with WordPress native notice styling 308 const $message = $('<div class="notice notice-' + type + ' is-dismissible iwc-simple-message"><p>' + text + '</p></div>'); 309 310 // Insert after the submit button in the current form 311 $form.find('.button').after($message); 312 313 // Auto-remove after 5 seconds for success messages 314 if (type === 'success') { 315 setTimeout(function() { 316 $message.fadeOut(500, function() { 317 $(this).remove(); 318 }); 319 }, 5000); 320 } 321 } 322 323 function revealApiKey($button, $input) { 324 // Show loading state 325 const originalText = $button.text(); 326 $button.text('Loading...').prop('disabled', true); 327 328 // Make AJAX request to fetch the API key 329 $.post(iwc_ajax.ajax_url, { 330 action: 'iwc_reveal_api_key', 331 nonce: iwc_ajax.reveal_nonce 332 }) 333 .done(function(response) { 334 if (response.success) { 335 const revealedKey = response.data.api_key; 336 337 // Update input and button state 338 $input.val(revealedKey).attr('data-state', 'revealed'); 339 $button.text('Hide').data('state', 'revealed').prop('disabled', false); 340 341 // Add countdown display 342 const $countdown = $('<span class="iwc-countdown">Auto-hide in 30s</span>'); 343 $button.after($countdown); 344 345 // Countdown timer 346 let secondsLeft = 30; 347 countdownInterval = setInterval(function() { 348 secondsLeft--; 349 $countdown.text(`Auto-hide in ${secondsLeft}s`); 350 351 if (secondsLeft <= 0) { 352 clearInterval(countdownInterval); 353 } 354 }, 1000); 355 356 // Auto-hide after 30 seconds 357 apiKeyTimeout = setTimeout(function() { 358 clearInterval(countdownInterval); 359 hideApiKey($button, $input); 360 }, 30000); 361 } else { 362 showMessage('Error: ' + (response.data || 'Failed to retrieve API key'), 'error'); 363 $button.text(originalText).prop('disabled', false); 364 } 365 }) 366 .fail(function() { 367 showMessage('Failed to retrieve API key. Please try again.', 'error'); 368 $button.text(originalText).prop('disabled', false); 369 }); 370 } 371 372 function hideApiKey($button, $input) { 373 const maskedKey = $input.data('masked'); 374 $input.val(maskedKey).removeAttr('data-state'); 375 $button.text('Reveal').data('state', 'masked').removeClass('iwc-hide-btn').addClass('iwc-reveal-btn'); 376 377 // Remove countdown display 378 $button.siblings('.iwc-countdown').remove(); 379 380 // Clear intervals and timeouts 381 if (countdownInterval) { 382 clearInterval(countdownInterval); 383 countdownInterval = null; 384 } 385 if (apiKeyTimeout) { 386 clearTimeout(apiKeyTimeout); 387 apiKeyTimeout = null; 388 } 389 } 390 5 391 $('#iwc-api-key-value').on('click', function() { 6 392 $(this).select(); 7 393 }); 8 394 9 // to display tabs 10 $("#imapie_tabs").tabs(); 11 12 // to show waiting animation of the curson when saving 13 $('#imapie_tabs #submit').click(function (e) { 395 // Initialize tabs functionality 396 initializeTabs(); 397 398 function initializeTabs() { 399 // Native tab functionality 400 $('.nav-tab').on('click', function(e) { 401 e.preventDefault(); 402 403 var targetTab = $(this).data('tab'); 404 405 // Remove active class from all tabs and content 406 $('.nav-tab').removeClass('nav-tab-active'); 407 $('.iwc-tab-content').removeClass('iwc-tab-active'); 408 409 // Add active class to clicked tab 410 $(this).addClass('nav-tab-active'); 411 412 // Show corresponding content 413 $('#iwc-tab-' + targetTab).addClass('iwc-tab-active'); 414 415 // Store active tab in sessionStorage 416 sessionStorage.setItem('iwc-active-tab', targetTab); 417 418 // Update ARIA attributes for accessibility 419 $('.nav-tab').attr('aria-selected', 'false'); 420 $(this).attr('aria-selected', 'true'); 421 422 $('.iwc-tab-content').attr('aria-hidden', 'true'); 423 $('#iwc-tab-' + targetTab).attr('aria-hidden', 'false'); 424 }); 425 426 // Restore active tab from sessionStorage on page load 427 var activeTab = sessionStorage.getItem('iwc-active-tab'); 428 if (activeTab && $('#iwc-tab-' + activeTab).length) { 429 $('.nav-tab').removeClass('nav-tab-active'); 430 $('.iwc-tab-content').removeClass('iwc-tab-active'); 431 432 $('[data-tab="' + activeTab + '"]').addClass('nav-tab-active'); 433 $('#iwc-tab-' + activeTab).addClass('iwc-tab-active'); 434 435 // Update ARIA attributes 436 $('.nav-tab').attr('aria-selected', 'false'); 437 $('[data-tab="' + activeTab + '"]').attr('aria-selected', 'true'); 438 439 $('.iwc-tab-content').attr('aria-hidden', 'true'); 440 $('#iwc-tab-' + activeTab).attr('aria-hidden', 'false'); 441 } else { 442 // Ensure first tab is active if no stored tab 443 $('.nav-tab:first').addClass('nav-tab-active'); 444 $('.iwc-tab-content:first').addClass('iwc-tab-active'); 445 446 // Set ARIA attributes for default state 447 $('.nav-tab:first').attr('aria-selected', 'true'); 448 $('.nav-tab:not(:first)').attr('aria-selected', 'false'); 449 $('.iwc-tab-content:first').attr('aria-hidden', 'false'); 450 $('.iwc-tab-content:not(:first)').attr('aria-hidden', 'true'); 451 } 452 } 453 454 // to show waiting animation of the cursor when saving 455 $('.iwc-tab-content .button').click(function (e) { 456 e.preventDefault(); 457 458 // Get the form within the active tab 459 var $activeTab = $('.iwc-tab-content.iwc-tab-active'); 460 var $form = $activeTab.find('form'); 461 462 // Validate form before submission 463 var hasErrors = false; 464 var $requiredFields = $form.find('[required]'); 465 466 $requiredFields.each(function() { 467 var $field = $(this); 468 if (!$field.val() || $field.val().trim() === '') { 469 $field.addClass('error'); 470 hasErrors = true; 471 } else { 472 $field.removeClass('error'); 473 } 474 }); 475 476 if (hasErrors) { 477 alert('Please fill in all required fields.'); 478 return false; 479 } 480 14 481 $('.imapie_settings_container').addClass('wait'); 15 $.when( 16 $.post('options.php', $('#impaie_form_post').serialize()), 17 $.post('options.php', $('#impaie_form_user').serialize()), 18 $.post('options.php', $('#impaie_form_comment').serialize()), 19 $.post('options.php', $('#impaie_form_term').serialize()) 20 ).done(function (a1, a2, a3, a4) { 21 $('.imapie_settings_container').removeClass('wait'); 22 }); 482 483 // Submit the active form 484 var formData = $form.serialize(); 485 486 $.post('options.php', formData) 487 .done(function() { 488 $('.imapie_settings_container').removeClass('wait'); 489 // Show success message after the submit button 490 showTabMessage('Settings saved successfully.', 'success', $form); 491 }) 492 .fail(function(xhr, status, error) { 493 $('.imapie_settings_container').removeClass('wait'); 494 console.error('Form submission failed:', error); 495 // Show error message after the submit button 496 showTabMessage('Error saving settings. Please try again.', 'error', $form); 497 }); 23 498 24 499 return false; 25 500 }); 26 501 502 // Custom Taxonomies form submission handling 503 $('#impaie_form_taxonomy .button').click(function (e) { 504 e.preventDefault(); 505 506 // Get the Custom Taxonomies form 507 var $form = $('#impaie_form_taxonomy'); 508 509 // Validate form before submission 510 var hasErrors = false; 511 var $requiredFields = $form.find('[required]'); 512 513 $requiredFields.each(function() { 514 var $field = $(this); 515 if (!$field.val() || $field.val().trim() === '') { 516 $field.addClass('error'); 517 hasErrors = true; 518 } else { 519 $field.removeClass('error'); 520 } 521 }); 522 523 if (hasErrors) { 524 alert('Please fill in all required fields.'); 525 return false; 526 } 527 528 $('.imapie_settings_container').addClass('wait'); 529 530 // Submit the form 531 var formData = $form.serialize(); 532 533 $.post('options.php', formData) 534 .done(function() { 535 $('.imapie_settings_container').removeClass('wait'); 536 // Show success message after the submit button 537 showSimpleMessage('Settings saved successfully.', 'success', $form); 538 }) 539 .fail(function(xhr, status, error) { 540 $('.imapie_settings_container').removeClass('wait'); 541 console.error('Form submission failed:', error); 542 // Show error message after the submit button 543 showSimpleMessage('Error saving settings. Please try again.', 'error', $form); 544 }); 545 546 return false; 547 }); 548 27 549 $('.uncheck_all').click(function (e) { 28 let uncheckAllStatus = $(this).attr('data-status'); 29 30 if (uncheckAllStatus == 0) { 31 $(this).attr('data-status', 1); 550 e.preventDefault(); 551 552 let $button = $(this); 553 let uncheckAllStatus = $button.attr('data-status') || '0'; 554 let isChecking = uncheckAllStatus === '0'; 555 556 if (isChecking) { 557 $button.attr('data-status', '1'); 558 $button.text($button.data('uncheck-text') || 'Uncheck All'); 32 559 } else { 33 $(this).attr('data-status', 0); 34 } 35 36 $(this).closest('form').find('input[type="checkbox"]').each(function () { 37 if (uncheckAllStatus == 0) { 38 $(this).prop('checked', true); 39 } else { 40 $(this).prop('checked', false); 41 } 42 }); 560 $button.attr('data-status', '0'); 561 $button.text($button.data('check-text') || 'Check All'); 562 } 563 564 // Target checkboxes in the current active tab only 565 var $activeTab = $('.iwc-tab-content.iwc-tab-active'); 566 if ($activeTab.length) { 567 $activeTab.find('input[type="checkbox"]').each(function () { 568 $(this).prop('checked', isChecking); 569 }); 570 } else { 571 // Fallback for non-tabbed pages (like General Settings) 572 $button.closest('form').find('input[type="checkbox"]').each(function () { 573 $(this).prop('checked', isChecking); 574 }); 575 } 576 43 577 return false; 578 }); 579 580 // Add accessibility improvements 581 $('input[type="checkbox"]').on('change', function() { 582 $(this).attr('aria-checked', this.checked); 583 }); 584 585 // Add form validation feedback 586 $('form input, form select, form textarea').on('blur', function() { 587 var $field = $(this); 588 if ($field.is('[required]') && (!$field.val() || $field.val().trim() === '')) { 589 $field.addClass('error').attr('aria-invalid', 'true'); 590 } else { 591 $field.removeClass('error').attr('aria-invalid', 'false'); 592 } 44 593 }); 45 594 }); -
integromat-connector/trunk/class/class-api-token.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Api_Token { … … 26 28 public static function initiate() { 27 29 if ( self::get() == '' ) { 28 update_site_option( self::API_TOKEN_IDENTIFIER, self::generate( self::API_TOKEN_LENGTH ) ); 30 // Use WordPress secure password generation for better entropy 31 $secure_token = wp_generate_password( self::API_TOKEN_LENGTH, true, true ); 32 update_site_option( self::API_TOKEN_IDENTIFIER, $secure_token ); 29 33 } 30 34 } … … 36 40 */ 37 41 public static function is_valid( $token ) { 38 return ( $token == get_site_option( self::API_TOKEN_IDENTIFIER ) ); 42 // Use hash_equals to prevent timing attacks 43 $stored_token = get_site_option( self::API_TOKEN_IDENTIFIER ); 44 return hash_equals( $stored_token, $token ); 39 45 } 40 46 41 42 47 /** 43 * Generate random string48 * Regenerate API token 44 49 * 45 * @param int $length 46 * @param string $charlist 47 * @return string 50 * @return string New token 48 51 * @throws \Exception 49 52 */ 50 public static function generate( $length = 10, $charlist = '0-9a-z' ) { 51 $charlist = count_chars( 52 preg_replace_callback( 53 '#.-.#', 54 function ( $m ) { 55 return implode( '', range( $m[0][0], $m[0][2] ) ); 56 }, 57 $charlist 58 ), 59 3 60 ); 61 $ch_len = strlen( $charlist ); 62 $res = ''; 63 for ( $i = 0; $i < $length; $i++ ) { 64 $res .= $charlist[ random_int( 0, $ch_len - 1 ) ]; 65 } 66 return $res; 53 public static function regenerate() { 54 $new_token = wp_generate_password( self::API_TOKEN_LENGTH, true, true ); 55 update_site_option( self::API_TOKEN_IDENTIFIER, $new_token ); 56 return $new_token; 67 57 } 68 58 -
integromat-connector/trunk/class/class-guard.php
r2793954 r3361722 1 1 <?php 2 2 namespace Integromat; 3 4 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 3 5 4 6 class Guard { … … 8 10 * @return bool 9 11 */ 10 public static function is_protected() { 11 $entities = array( 'posts', 'users', 'comments', 'tags', 'categories', 'media' ); 12 $json_base = str_replace( get_site_url(), '', get_rest_url( null, 'wp/v2/' ) ); 13 $endpoint = str_replace( $json_base, '', sanitize_url( $_SERVER['REQUEST_URI'] ) ); 14 $f = explode( '/', $endpoint ); 15 return in_array( $f[0], $entities, true ) && in_array( $_SERVER['REQUEST_METHOD'], array( 'POST', 'PUT', 'DELETE' ) ); 12 public static function is_protected() { 13 // Only guard if IWC-API-KEY header is present 14 if ( ! isset( $_SERVER['HTTP_IWC_API_KEY'] ) || empty( $_SERVER['HTTP_IWC_API_KEY'] ) ) { 15 return false; // No protection if no IWC-API-KEY header 16 } 17 18 // Validate required server variables 19 if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! isset( $_SERVER['REQUEST_METHOD'] ) ) { 20 return true; // Err on the side of caution 21 } 22 23 $entities = array( 'posts', 'users', 'comments', 'tags', 'categories', 'media' ); 24 $json_base = str_replace( get_site_url(), '', get_rest_url( null, 'wp/v2/' ) ); 25 $request_uri = sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ); 26 $endpoint = str_replace( $json_base, '', $request_uri ); 27 $request_method = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); 28 29 // Parse endpoint safely 30 $endpoint_parts = explode( '/', trim( $endpoint, '/' ) ); 31 $first_part = isset( $endpoint_parts[0] ) ? sanitize_text_field( $endpoint_parts[0] ) : ''; 32 33 $protected_methods = array( 'POST', 'PUT', 'DELETE', 'PATCH' ); 34 35 return in_array( $first_part, $entities, true ) && in_array( $request_method, $protected_methods, true ); 16 36 } 17 37 } -
integromat-connector/trunk/class/class-logger.php
r2784613 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Logger { 6 8 const MAXFILESIZEMB = 5; 7 const CIPHERMETHOD = 'AES-256-ECB'; 8 9 const CIPHERMETHOD = 'AES-256-CBC'; // More secure than ECB mode 10 const ENCRYPTION_KEY_LENGTH = 32; 11 const BYTES_IN_MB = 1000000; 12 const API_KEY_PREVIEW_LENGTH = 5; 13 14 /** 15 * Get secure log file location outside web root 16 * 17 * @return string 18 */ 9 19 private static function get_file_location() { 10 return WP_CONTENT_DIR . '/uploads/iwclog.dat'; 20 // Store logs outside web-accessible directory for security 21 $upload_dir = wp_upload_dir(); 22 $log_dir = $upload_dir['basedir'] . '/iwc-logs'; 23 24 // Create directory if it doesn't exist 25 if ( ! file_exists( $log_dir ) ) { 26 wp_mkdir_p( $log_dir ); 27 // Add .htaccess to deny web access 28 file_put_contents( $log_dir . '/.htaccess', "Deny from all\n" ); 29 // Add index.php to prevent directory listing 30 file_put_contents( $log_dir . '/index.php', "<?php\n// Silence is golden.\n" ); 31 } 32 33 return $log_dir . '/iwclog.dat'; 34 } 35 36 /** 37 * Get encryption key for log data 38 * 39 * @return string 40 */ 41 private static function get_encryption_key() { 42 $key = get_site_option( 'iwc_log_encryption_key' ); 43 if ( empty( $key ) ) { 44 // Generate a new encryption key 45 $key = wp_generate_password( self::ENCRYPTION_KEY_LENGTH, true, true ); 46 update_site_option( 'iwc_log_encryption_key', $key ); 47 } 48 return $key; 49 } 50 51 /** 52 * Generate IV for encryption 53 * 54 * @return string 55 */ 56 private static function generate_iv() { 57 return openssl_random_pseudo_bytes( openssl_cipher_iv_length( self::CIPHERMETHOD ) ); 11 58 } 12 59 … … 16 63 } else { 17 64 $fsize = filesize( self::get_file_location() ); 18 if ( $fsize > ( self::MAXFILESIZEMB * 1000000) ) {65 if ( $fsize > ( self::MAXFILESIZEMB * self::BYTES_IN_MB ) ) { 19 66 self::create_file(); 20 67 } … … 23 70 24 71 private static function create_file() { 25 $init = 'Log file initiated @ ' . date( 'Y-m-d G:i:s' ) . "\n=SERVER INFO START="; 26 $server_data = $_SERVER; 27 $server_data['REQUEST_URI'] = self::strip_request_query( sanitize_url( $_SERVER['REQUEST_URI'] ) ); 28 $server_data['HTTP_IWC_API_KEY'] = ( isset( $server_data['HTTP_IWC_API_KEY'] ) ? substr( sanitize_text_field( $_SERVER['HTTP_IWC_API_KEY'] ), 0, 5 ) . '...' : 'Not Provided' ); 29 30 $server_data['SERVER_SOFTWARE'] = sanitize_text_field( $_SERVER['SERVER_SOFTWARE'] ); 31 $server_data['REQUEST_URI'] = sanitize_url( $_SERVER['REQUEST_URI'] ); 32 $server_data['REDIRECT_UNIQUE_ID'] = sanitize_text_field( $_SERVER['REDIRECT_UNIQUE_ID'] ); 33 34 $server_data['REDIRECT_STATUS'] = sanitize_text_field( $_SERVER['REDIRECT_STATUS'] ); 35 $server_data['UNIQUE_ID'] = sanitize_text_field( $_SERVER['UNIQUE_ID'] ); 36 $server_data['HTTP_X_DATADOG_SAMPLING_PRIORITY'] = sanitize_text_field( $_SERVER['HTTP_X_DATADOG_SAMPLING_PRIORITY'] ); 37 $server_data['HTTP_X_DATADOG_SAMPLED'] = sanitize_text_field( $_SERVER['HTTP_X_DATADOG_SAMPLED'] ); 38 $server_data['HTTP_X_DATADOG_PARENT_ID'] = sanitize_text_field( $_SERVER['HTTP_X_DATADOG_PARENT_ID'] ); 39 40 $server_data['HTTP_X_DATADOG_TRACE_ID'] = sanitize_text_field( $_SERVER['HTTP_X_DATADOG_TRACE_ID'] ); 41 $server_data['CONTENT_TYPE'] = sanitize_text_field( $_SERVER['CONTENT_TYPE'] ); 42 $server_data['HTTP_USER_AGENT'] = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ); 43 $server_data['HTTP_X_FORWARDED_PORT'] = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_PORT'] ); 44 45 $server_data['HTTP_X_FORWARDED_SSL'] = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_SSL'] ); 46 $server_data['HTTP_X_FORWARDED_PROTO'] = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_PROTO'] ); 47 $server_data['HTTP_X_FORWARDED_FOR'] = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_FOR'] ); 48 $server_data['HTTP_X_REAL_IP'] = sanitize_text_field( $_SERVER['HTTP_X_REAL_IP'] ); 49 $server_data['HTTP_CONNECTION'] = sanitize_text_field( $_SERVER['HTTP_CONNECTION'] ); 50 $server_data['HTTP_HOST'] = sanitize_text_field( $_SERVER['HTTP_HOST'] ); 51 $server_data['HTTP_X_FORWARDED_HOST'] = sanitize_text_field( $_SERVER['HTTP_X_FORWARDED_HOST'] ); 52 $server_data['PATH'] = sanitize_text_field( $_SERVER['PATH'] ); 53 $server_data['DYLD_LIBRARY_PATH'] = sanitize_text_field( $_SERVER['DYLD_LIBRARY_PATH'] ); 54 $server_data['SERVER_SIGNATURE'] = sanitize_text_field( $_SERVER['SERVER_SIGNATURE'] ); 55 $server_data['SERVER_NAME'] = sanitize_text_field( $_SERVER['SERVER_NAME'] ); 56 $server_data['SERVER_ADDR'] = sanitize_text_field( $_SERVER['SERVER_ADDR'] ); 57 $server_data['SERVER_PORT'] = sanitize_text_field( $_SERVER['SERVER_PORT'] ); 58 $server_data['REMOTE_ADDR'] = sanitize_text_field( $_SERVER['REMOTE_ADDR'] ); 59 $server_data['DOCUMENT_ROOT'] = sanitize_text_field( $_SERVER['DOCUMENT_ROOT'] ); 60 $server_data['REQUEST_SCHEME'] = sanitize_text_field( $_SERVER['REQUEST_SCHEME'] ); 61 $server_data['CONTEXT_PREFIX'] = sanitize_text_field( $_SERVER['CONTEXT_PREFIX'] ); 62 $server_data['CONTEXT_DOCUMENT_ROOT'] = sanitize_text_field( $_SERVER['CONTEXT_DOCUMENT_ROOT'] ); 63 $server_data['SERVER_ADMIN'] = sanitize_email( $_SERVER['SERVER_ADMIN'] ); 64 $server_data['SCRIPT_FILENAME'] = sanitize_text_field( $_SERVER['SCRIPT_FILENAME'] ); 65 $server_data['REMOTE_PORT'] = sanitize_text_field( $_SERVER['REMOTE_PORT'] ); 66 $server_data['REDIRECT_URL'] = sanitize_text_field( $_SERVER['REDIRECT_URL'] ); 67 $server_data['GATEWAY_INTERFACE'] = sanitize_text_field( $_SERVER['GATEWAY_INTERFACE'] ); 68 $server_data['SERVER_PROTOCOL'] = sanitize_text_field( $_SERVER['SERVER_PROTOCOL'] ); 69 $server_data['REQUEST_METHOD'] = sanitize_text_field( $_SERVER['REQUEST_METHOD'] ); 70 $server_data['SCRIPT_NAME'] = sanitize_text_field( $_SERVER['SCRIPT_NAME'] ); 71 $server_data['PHP_SELF'] = sanitize_text_field( $_SERVER['PHP_SELF'] ); 72 $server_data['REQUEST_TIME_FLOAT'] = sanitize_text_field( $_SERVER['REQUEST_TIME_FLOAT'] ); 73 $server_data['REQUEST_TIME'] = sanitize_text_field( $_SERVER['REQUEST_TIME'] ); 74 75 /* 76 unset( $server_data['QUERY_STRING'] ); 77 unset( $server_data['REDIRECT_QUERY_STRING'] ); 78 unset( $server_data['HTTP_AUTHORIZATION'] ); 79 unset( $server_data['REDIRECT_HTTP_AUTHORIZATION'] ); 80 unset( $server_data['HTTP_COOKIE'] ); 81 if ( isset( $server_data['PHP_AUTH_USER'] ) ) { 82 $server_data['PHP_AUTH_USER'] = '*******'; 83 } 84 if ( isset( $server_data['PHP_AUTH_PW'] ) ) { 85 $server_data['PHP_AUTH_PW'] = '*******'; 86 } 87 */ 88 $init .= str_replace( 'Array', '', print_r( $server_data, true ) ) . "=SERVER INFO END=\n\n"; 89 file_put_contents( self::get_file_location(), self::encrypt( $init ) ); 72 // Create an empty log file without server info or CSV header 73 file_put_contents( self::get_file_location(), self::encrypt( '' ) ); 90 74 if ( ! self::file_exists() ) { 91 die( '{"code": "log_write_fail", "message": "Log file can not be created. Check permissions."}' ); 92 } 75 wp_die( wp_json_encode( array( 'code' => 'log_write_fail', 'message' => 'Log file can not be created. Check permissions.' ) ) ); 76 } 77 } 78 79 /** 80 * Generate server info and CSV header for download 81 * 82 * @return string 83 */ 84 private static function get_server_info_and_header() { 85 $init = "====== SERVER INFO START ======\n\n"; 86 $server_data = array(); 87 88 // Safely extract server data with existence checks and unslashing 89 $server_data['REQUEST_URI'] = isset( $_SERVER['REQUEST_URI'] ) ? self::strip_request_query( sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : 'Not Available'; 90 $server_data['HTTP_IWC_API_KEY'] = isset( $_SERVER['HTTP_IWC_API_KEY'] ) ? substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_IWC_API_KEY'] ) ), 0, self::API_KEY_PREVIEW_LENGTH ) . '...' : 'Not Provided'; 91 92 // List of server variables to extract using the helper method 93 $server_vars = array( 94 'SERVER_SOFTWARE', 'REDIRECT_UNIQUE_ID', 'REDIRECT_STATUS', 'UNIQUE_ID', 95 'HTTP_X_DATADOG_SAMPLING_PRIORITY', 'HTTP_X_DATADOG_SAMPLED', 'HTTP_X_DATADOG_PARENT_ID', 96 'HTTP_X_DATADOG_TRACE_ID', 'CONTENT_TYPE', 'HTTP_USER_AGENT', 'HTTP_X_FORWARDED_PORT', 97 'HTTP_X_FORWARDED_SSL', 'HTTP_X_FORWARDED_PROTO', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 98 'HTTP_CONNECTION', 'HTTP_HOST', 'HTTP_X_FORWARDED_HOST', 'PATH', 'DYLD_LIBRARY_PATH', 99 'SERVER_SIGNATURE', 'SERVER_NAME', 'SERVER_ADDR', 'SERVER_PORT', 'REMOTE_ADDR', 100 'DOCUMENT_ROOT', 'REQUEST_SCHEME', 'CONTEXT_PREFIX', 'CONTEXT_DOCUMENT_ROOT', 101 'SCRIPT_FILENAME', 'REMOTE_PORT', 'REDIRECT_URL', 'GATEWAY_INTERFACE', 102 'SERVER_PROTOCOL', 'REQUEST_METHOD', 'SCRIPT_NAME', 'PHP_SELF', 'REQUEST_TIME_FLOAT', 'REQUEST_TIME' 103 ); 104 105 // Handle all server variables using helper method 106 foreach ( $server_vars as $var ) { 107 $server_data[ $var ] = self::get_sanitized_server_var( $var ); 108 } 109 110 // Special handling for SERVER_ADMIN using email sanitization 111 $server_data['SERVER_ADMIN'] = isset( $_SERVER['SERVER_ADMIN'] ) ? sanitize_email( wp_unslash( $_SERVER['SERVER_ADMIN'] ) ) : 'Not Available'; 112 113 foreach ( $server_data as $key => $value ) { 114 $init .= $key . ': ' . $value . "\n"; 115 } 116 $init .= "\n====== SERVER INFO END ======\n\n"; 117 $init .= "date,method,uri,ip,codes,logged_in\n"; 118 119 return $init; 93 120 } 94 121 … … 97 124 } 98 125 126 /** 127 * Encrypt data using secure AES-256-CBC 128 * 129 * @param string $data 130 * @return string 131 */ 99 132 private static function encrypt( $data ) { 100 return openssl_encrypt( $data, self::CIPHERMETHOD, self::get_enc_key() ); 101 } 102 133 $key = self::get_encryption_key(); 134 $iv = self::generate_iv(); 135 136 $encrypted = openssl_encrypt( $data, self::CIPHERMETHOD, $key, 0, $iv ); 137 138 // Prepend IV to encrypted data 139 return base64_encode( $iv . $encrypted ); 140 } 141 142 /** 143 * Decrypt data using secure AES-256-CBC 144 * 145 * @param string $data 146 * @return string 147 */ 103 148 private static function decrypt( $data ) { 104 return openssl_decrypt( $data, self::CIPHERMETHOD, self::get_enc_key() ); 149 $key = self::get_encryption_key(); 150 $data = base64_decode( $data ); 151 152 $iv_length = openssl_cipher_iv_length( self::CIPHERMETHOD ); 153 $iv = substr( $data, 0, $iv_length ); 154 $encrypted = substr( $data, $iv_length ); 155 156 return openssl_decrypt( $encrypted, self::CIPHERMETHOD, $key, 0, $iv ); 157 } 158 159 /** 160 * Helper method to safely get and sanitize server variables 161 * 162 * @param string $var_name The server variable name 163 * @return string Sanitized value or 'Not Available' 164 */ 165 private static function get_sanitized_server_var( $var_name ) { 166 return isset( $_SERVER[ $var_name ] ) ? sanitize_text_field( wp_unslash( $_SERVER[ $var_name ] ) ) : 'Not Available'; 105 167 } 106 168 … … 111 173 112 174 private static function get_record( $codes ) { 175 $request_method = self::get_sanitized_server_var( 'REQUEST_METHOD' ); 176 if ( $request_method === 'Not Available' ) { 177 $request_method = 'Unknown'; 178 } 179 180 $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? self::strip_request_query( sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : 'Unknown'; 181 $remote_addr = self::get_sanitized_server_var( 'REMOTE_ADDR' ); 182 if ( $remote_addr === 'Not Available' ) { 183 $remote_addr = 'Unknown'; 184 } 185 113 186 $r = array( 114 'request' => sanitize_text_field( $_SERVER['REQUEST_METHOD'] ) . ' ' . self::strip_request_query( sanitize_url( $_SERVER['REQUEST_URI'] ) ), 115 'ip' => sanitize_url( $_SERVER['REMOTE_ADDR'] ), 116 'codes' => $codes . '(' . (string) is_user_logged_in() . ')', 187 'date' => gmdate( 'Y-m-d\TH:i:s.v\Z' ), 188 'method' => $request_method, 189 'uri' => $request_uri, 190 'ip' => $remote_addr, 191 'codes' => $codes, 192 'logged_in' => (string) is_user_logged_in(), 117 193 ); 118 $r = str_replace( array( '[', 'Array', ']' ), '', print_r( $r, true ) ); 119 $r = str_replace( ' =>', ':', $r ); 120 return date( 'Y-m-d G:i:s' ) . ' ' . $r . "\n"; 194 return trim( implode(',', $r) ) . "\n"; 121 195 } 122 196 … … 130 204 public static function get_plain_file_content() { 131 205 if ( ! file_exists( self::get_file_location() ) ) { 132 die( '{"code": "log_read_fail", "message": "Log file does not exist."}');206 wp_die( wp_json_encode( array( 'code' => 'log_read_fail', 'message' => 'Log file does not exist.' ) ) ); 133 207 } 134 208 $enc_data = file_get_contents( self::get_file_location() ); … … 136 210 } 137 211 138 private static function get_enc_key() {139 $key = get_option( 'iwc_api_key' );140 if ( empty( $key ) ) {141 file_put_contents( self::get_file_location(), 'iwc_api_key Not Found' );142 }143 return $key;144 }145 146 212 public static function download() { 147 213 $log_data = self::get_plain_file_content(); 214 215 // Prepend server info and CSV header to the log data for download 216 $download_data = self::get_server_info_and_header() . $log_data; 217 148 218 header( 'Content-Type: application/octet-stream' ); 149 219 header( 'Content-Transfer-Encoding: Binary' ); 150 220 header( 'Content-disposition: attachment; filename="log.txt"' ); 151 echo $log_data; 221 222 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We want to output raw log data 223 echo $download_data; 152 224 exit; 153 225 } 226 227 /** 228 * Purge all log data 229 * 230 * @return bool Success status 231 */ 232 public static function purge() { 233 $log_file = self::get_file_location(); 234 235 if ( file_exists( $log_file ) ) { 236 // Remove the log file using WordPress function 237 $result = wp_delete_file( $log_file ); 238 239 if ( $result ) { 240 // Also remove the encryption key to ensure complete cleanup 241 delete_site_option( 'iwc_log_encryption_key' ); 242 return true; 243 } 244 return false; 245 } 246 247 // If file doesn't exist, consider it a success 248 return true; 249 } 154 250 } -
integromat-connector/trunk/class/class-rest-request.php
r2915666 r3361722 3 3 namespace Integromat; 4 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 6 5 7 class Rest_Request { 6 8 7 public static function dispatch() { 8 preg_match( '#\/wp-json/(.*)\??.*#i', $_SERVER['REQUEST_URI'], $route_match ); 9 if ( ! isset( $route_match[1] ) ) { 10 return; 11 } 12 $f = explode( '?', $route_match[1] ); 13 $rest_route = '/' . $f[0]; 14 15 // Authentication isn’t performed when making internal requests. 16 $request = new \WP_REST_Request( $_SERVER['REQUEST_METHOD'], $rest_route ); 17 $request->set_query_params( $_GET ); 18 19 if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) { 20 $body = json_decode( file_get_contents( 'php://input' ), true ); 21 $request->set_body_params( $body ); 22 9 public static function dispatch() { 10 // Check payload size early 11 if ( \Integromat\Rate_Limiter::is_payload_too_large() ) { 12 Rest_Response::render_error( 413, 'Request payload too large', 'payload_too_large' ); 13 return; 14 } 15 16 // Add authentication check for security (use API-specific permissions) 17 if ( ! current_user_can( 'iwc_read_posts' ) ) { 18 Rest_Response::render_error( 403, 'Insufficient API permissions', 'rest_forbidden' ); 19 return; 20 } 21 22 // Validate and sanitize REQUEST_URI 23 if ( ! isset( $_SERVER['REQUEST_URI'] ) || empty( $_SERVER['REQUEST_URI'] ) ) { 24 Rest_Response::render_error( 400, 'Invalid request URI', 'rest_invalid_request' ); 25 return; 26 } 27 28 $request_uri = sanitize_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ); 29 30 // Extract the REST route from the request URI with better validation 31 if ( ! preg_match( '#\/wp-json/(.*?)(\?.*)?$#i', $request_uri, $route_match ) ) { 32 Rest_Response::render_error( 400, 'Invalid REST API request', 'rest_invalid_route' ); 33 return; 34 } 35 36 if ( ! isset( $route_match[1] ) || empty( $route_match[1] ) ) { 37 Rest_Response::render_error( 400, 'Missing REST route', 'rest_missing_route' ); 38 return; 39 } 40 41 $rest_route = '/' . sanitize_text_field( $route_match[1] ); 42 43 // Validate HTTP method 44 $allowed_methods = array( 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ); 45 $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; 46 47 if ( ! in_array( $request_method, $allowed_methods, true ) ) { 48 Rest_Response::render_error( 405, 'Method not allowed', 'rest_method_not_allowed' ); 49 return; 50 } 51 52 // Authentication isn't performed when making internal requests. 53 $request = new \WP_REST_Request( $request_method, $rest_route ); 54 55 // Sanitize and validate query parameters 56 $query_params = array(); 57 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 58 if ( ! empty( $_GET ) ) { 59 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 60 foreach ( $_GET as $key => $value ) { 61 $clean_key = sanitize_key( $key ); 62 if ( is_array( $value ) ) { 63 $query_params[ $clean_key ] = array_map( 'sanitize_text_field', wp_unslash( $value ) ); 64 } else { 65 $query_params[ $clean_key ] = sanitize_text_field( wp_unslash( $value ) ); 66 } 67 } 68 } 69 $request->set_query_params( $query_params ); 70 71 if ( 'POST' === $request_method ) { 72 $input = file_get_contents( 'php://input' ); 73 if ( false === $input ) { 74 Rest_Response::render_error( 400, 'Unable to read request body', 'rest_invalid_request' ); 75 return; 76 } 77 78 // Validate JSON if not empty 79 if ( ! empty( $input ) ) { 80 $body = json_decode( $input, true ); 81 if ( json_last_error() !== JSON_ERROR_NONE ) { 82 Rest_Response::render_error( 400, 'Invalid JSON in request body: ' . json_last_error_msg(), 'rest_invalid_json' ); 83 return; 84 } 85 86 // Sanitize body data 87 $body = self::sanitize_recursive( $body ); 88 $request->set_body_params( $body ); 89 } 90 91 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST API endpoint, authentication handled separately 23 92 if ( ! empty( $_FILES['file'] ) ) { 24 93 self::upload_media(); 94 return; // upload_media handles its own response 25 95 } 26 96 } … … 30 100 $response_data = $server->response_to_data( $response, false ); 31 101 32 // Save custom meta. 33 if ( 'POST' === $_SERVER['REQUEST_METHOD'] ) { 34 if ( ! empty( $body['meta'] ) ) { 35 $content_type = self::get_content_type( $rest_route ); 36 self::update_meta( $response_data['id'], $content_type, $body['meta'] ); 37 } 38 } 102 // Save custom meta for POST requests 103 if ( 'POST' === $request_method && ! empty( $body['meta'] ) ) { 104 $content_type = self::get_content_type( $rest_route ); 105 if ( isset( $response_data['id'] ) && is_numeric( $response_data['id'] ) ) { 106 self::update_meta( absint( $response_data['id'] ), $content_type, $body['meta'] ); 107 } 108 } 109 39 110 self::send_response( $response, $response_data ); 111 } 112 113 /** 114 * Recursively sanitize array data 115 * 116 * @param mixed $data 117 * @return mixed 118 */ 119 private static function sanitize_recursive( $data ) { 120 if ( is_array( $data ) ) { 121 $sanitized = array(); 122 foreach ( $data as $key => $value ) { 123 $clean_key = sanitize_key( $key ); 124 $sanitized[ $clean_key ] = self::sanitize_recursive( $value ); 125 } 126 return $sanitized; 127 } elseif ( is_string( $data ) ) { 128 return sanitize_text_field( $data ); 129 } elseif ( is_numeric( $data ) ) { 130 return is_float( $data ) ? floatval( $data ) : intval( $data ); 131 } elseif ( is_bool( $data ) ) { 132 return (bool) $data; 133 } 134 135 return $data; 40 136 } 41 137 … … 46 142 */ 47 143 private static function send_response( $response, $response_data ) { 48 $response_json = wp_json_encode( $response_data ); 144 // Add security headers 145 header( 'X-Content-Type-Options: nosniff' ); 146 header( 'X-Frame-Options: DENY' ); 147 header( 'X-XSS-Protection: 1; mode=block' ); 148 header( 'Referrer-Policy: strict-origin-when-cross-origin' ); 149 49 150 if ( ! empty( $response->headers ) ) { 50 151 foreach ( $response->headers as $key => $val ) { 51 header( "$key: $val" ); 52 } 53 } 54 header( 'Content-type: application/json' ); 152 // Sanitize headers to prevent header injection attacks 153 $clean_key = preg_replace('/[^\w-]/', '', $key); 154 $clean_val = preg_replace('/[\r\n]/', '', $val); 155 if ( ! empty( $clean_key ) && ! empty( $clean_val ) ) { 156 header( "$clean_key: $clean_val" ); 157 } 158 } 159 } 160 header( 'Content-type: application/json; charset=utf-8' ); 55 161 if ( is_object( $response_data ) && is_object( $response_data->data ) && (int) $response_data->data->status > 0 ) { 56 162 http_response_code( $response_data->data->status ); 57 163 } 58 die( $response_json ); 164 165 // Respond with JSON-encoded data and exit 166 echo wp_json_encode( $response_data ); 167 exit(); 59 168 } 60 169 61 170 62 171 private static function upload_media() { 63 $udir = wp_upload_dir(); 64 $media_file_source = $udir['path'] . '/' . sanitize_file_name( $_FILES['file']['name'] ); 65 66 if ( (int) $_FILES['file']['size'] === 0 ) { 67 Rest_Response::render_error( 500, 'The uploaded file exceeds the upload_max_filesize directive in php.ini.', 'rest_upload_unknown_error' ); 68 } 69 70 if ( (int) $_FILES['file']['error'] > 0 ) { 71 Rest_Response::render_error( 500, 'An error has occured when uploading file to the server.', 'rest_upload_unknown_error' ); 72 } 73 74 copy( realpath( $_FILES['file']['tmp_name'] ), $media_file_source ); 75 76 $title = isset( $_REQUEST['title'] ) ? sanitize_title( $_REQUEST['title'] ) : ''; 77 $description = isset( $_REQUEST['description'] ) ? sanitize_text_field( $_REQUEST['description'] ) : ''; 78 $caption = isset( $_REQUEST['caption'] ) ? sanitize_text_field( $_REQUEST['caption'] ) : ''; 79 $alt_text = isset( $_REQUEST['alt_text'] ) ? sanitize_text_field( $_REQUEST['alt_text'] ) : ''; 80 $post_id = isset( $_REQUEST['post'] ) ? (int) $_REQUEST['post'] : ''; 81 82 $upload_dir = wp_upload_dir(); 83 $filename = basename( $media_file_source ); 84 if ( wp_mkdir_p( $upload_dir['path'] ) ) { 85 $file = $upload_dir['path'] . '/' . $filename; 172 // Add authentication check for file uploads using API-specific permissions 173 if ( ! current_user_can( 'iwc_upload_files' ) ) { 174 Rest_Response::render_error( 403, 'Insufficient permissions for file upload', 'rest_forbidden' ); 175 return; 176 } 177 178 // Validate file upload data exists and is properly formatted 179 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST API endpoint, authentication handled separately 180 if ( empty( $_FILES['file'] ) || ! is_array( $_FILES['file'] ) ) { 181 Rest_Response::render_error( 400, 'No file uploaded', 'rest_upload_no_file' ); 182 return; 183 } 184 185 // Check for upload errors first 186 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- REST API endpoint, authentication handled separately 187 $upload_error = isset( $_FILES['file']['error'] ) ? absint( $_FILES['file']['error'] ) : UPLOAD_ERR_NO_FILE; 188 189 if ( $upload_error !== UPLOAD_ERR_OK ) { 190 $error_messages = array( 191 UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive', 192 UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive', 193 UPLOAD_ERR_PARTIAL => 'File was only partially uploaded', 194 UPLOAD_ERR_NO_FILE => 'No file was uploaded', 195 UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder', 196 UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', 197 UPLOAD_ERR_EXTENSION => 'File upload stopped by extension', 198 ); 199 200 $error_message = isset( $error_messages[ $upload_error ] ) ? $error_messages[ $upload_error ] : 'Unknown upload error'; 201 Rest_Response::render_error( 400, $error_message, 'rest_upload_error' ); 202 return; 203 } 204 205 // Sanitize file upload data (preserve tmp_name as-is for file operations) 206 // phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REST API endpoint, authentication handled separately, tmp_name is a system-generated temporary file path, safe to use unsanitized 207 $uploaded_file = array( 208 'name' => isset( $_FILES['file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['file']['name'] ) ) : '', 209 'type' => isset( $_FILES['file']['type'] ) ? sanitize_mime_type( wp_unslash( $_FILES['file']['type'] ) ) : '', 210 'tmp_name' => isset( $_FILES['file']['tmp_name'] ) ? $_FILES['file']['tmp_name'] : '', // Keep tmp_name unsanitized for file operations 211 'error' => $upload_error, 212 'size' => isset( $_FILES['file']['size'] ) ? $_FILES['file']['size'] : 0, // Don't convert to int yet 213 ); 214 //phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized 215 216 // Validate file exists and has content using file system check 217 if ( empty( $uploaded_file['tmp_name'] ) || ! file_exists( $uploaded_file['tmp_name'] ) ) { 218 Rest_Response::render_error( 400, 'No file uploaded or file not found', 'rest_upload_no_file' ); 219 return; 220 } 221 222 // Get actual file size from filesystem (more reliable than $_FILES size) 223 $actual_file_size = filesize( $uploaded_file['tmp_name'] ); 224 if ( $actual_file_size === false || $actual_file_size === 0 ) { 225 Rest_Response::render_error( 400, 'File is empty or unreadable', 'rest_upload_no_file' ); 226 return; 227 } 228 229 // Update the size with actual file size 230 $uploaded_file['size'] = $actual_file_size; 231 232 // Use our enhanced file validator 233 $validation_result = \Integromat\File_Validator::validate_upload( $uploaded_file ); 234 235 if ( is_wp_error( $validation_result ) ) { 236 Rest_Response::render_error( 400, $validation_result->get_error_message(), $validation_result->get_error_code() ); 237 return; 238 } 239 240 // Use WordPress secure upload handling 241 $upload_overrides = array( 242 'test_form' => false, 243 'test_size' => true, 244 ); 245 246 // Move uploaded file using WordPress function 247 require_once ABSPATH . 'wp-admin/includes/file.php'; 248 $movefile = wp_handle_upload( $uploaded_file, $upload_overrides ); 249 250 if ( $movefile && ! isset( $movefile['error'] ) ) { 251 // Get additional metadata 252 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 253 $title = isset( $_REQUEST['title'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['title'] ) ) : ''; 254 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 255 $description = isset( $_REQUEST['description'] ) ? sanitize_textarea_field( wp_unslash( $_REQUEST['description'] ) ) : ''; 256 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 257 $caption = isset( $_REQUEST['caption'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['caption'] ) ) : ''; 258 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 259 $alt_text = isset( $_REQUEST['alt_text'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['alt_text'] ) ) : ''; 260 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST API endpoint, authentication handled separately 261 $post_id = isset( $_REQUEST['post'] ) ? absint( $_REQUEST['post'] ) : 0; 262 $filename = basename( $movefile['file'] ); 263 // Prepare attachment data 264 $attachment = array( 265 'post_mime_type' => $movefile['type'], 266 'post_title' => ( ! empty( $title ) ? $title : sanitize_file_name( $filename ) ), 267 'post_content' => $description, 268 'post_excerpt' => $caption, 269 'post_status' => 'inherit', 270 ); 271 272 // Insert attachment 273 $attachment_id = wp_insert_attachment( $attachment, $movefile['file'] ); 274 275 if ( is_wp_error( $attachment_id ) ) { 276 Rest_Response::render_error( 500, 'Failed to create attachment', 'rest_upload_attachment_error' ); 277 return; 278 } 279 280 // Set alt text if provided 281 if ( ! empty( $alt_text ) ) { 282 update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $alt_text ) ); 283 } 284 285 // Generate attachment metadata 286 require_once ABSPATH . 'wp-admin/includes/media.php'; 287 require_once ABSPATH . 'wp-admin/includes/image.php'; 288 $attachment_data = wp_generate_attachment_metadata( $attachment_id, $movefile['file'] ); 289 wp_update_attachment_metadata( $attachment_id, $attachment_data ); 290 291 // Relate to a post if specified 292 if ( $post_id > 0 && get_post( $post_id ) ) { 293 set_post_thumbnail( $post_id, $attachment_id ); 294 } 295 296 // Prepare response 297 $meta = wp_get_attachment_metadata( $attachment_id ); 298 $post = get_post( $attachment_id ); 299 if ( is_array( $meta ) ) { 300 $response_data = array_merge( $meta, (array) $post ); 301 } else { 302 $response_data = (array) $post; 303 } 304 305 self::send_response( (object) array(), $response_data ); 86 306 } else { 87 $file = $upload_dir['basedir'] . '/' . $filename; 88 } 89 90 $wp_file_type = wp_check_filetype( $filename, null ); 91 $allowed_types = get_allowed_mime_types(); 92 93 if ( ! in_array( $wp_file_type['type'], $allowed_types ) ) { 94 Rest_Response::render_error( 500, 'Sorry, this file type is not permitted for security reasons.', 'rest_upload_unknown_error' ); 95 } 96 97 $attachment = array( 98 'post_mime_type' => $wp_file_type['type'], 99 'post_title' => ( ! empty( $title ) ? $title : sanitize_file_name( $filename ) ), 100 'post_content' => ( ! empty( $description ) ? $description : '' ), 101 'post_excerpt' => ( ! empty( $caption ) ? $caption : '' ), 102 'post_status' => 'inherit', 307 $error_message = isset( $movefile['error'] ) ? sanitize_text_field( $movefile['error'] ) : 'Unknown upload error'; 308 Rest_Response::render_error( 500, 'Upload failed: ' . $error_message, 'rest_upload_unknown_error' ); 309 } 310 } 311 312 private static function update_meta( $content_id, $content_type, $meta_fields ) { 313 // Define meta function mapping for better maintainability 314 $meta_functions = array( 315 'comments' => array( 'update' => 'update_comment_meta', 'delete' => 'delete_comment_meta' ), 316 'tags' => array( 'update' => 'update_term_meta', 'delete' => 'delete_term_meta' ), 317 'categories' => array( 'update' => 'update_term_meta', 'delete' => 'delete_term_meta' ), 318 'users' => array( 'update' => 'update_user_meta', 'delete' => 'delete_user_meta' ), 319 'default' => array( 'update' => 'update_post_meta', 'delete' => 'delete_post_meta' ), 103 320 ); 104 $attachment_id = wp_insert_attachment( $attachment, $file ); 105 if ( ! empty( $alt_text ) ) { 106 update_post_meta( $attachment_id, '_wp_attachment_image_alt', $alt_text ); 107 } 108 109 require_once ABSPATH . 'wp-admin/includes/media.php'; 110 require_once ABSPATH . 'wp-admin/includes/image.php'; 111 $attachment_data = wp_generate_attachment_metadata( $attachment_id, $file ); 112 wp_update_attachment_metadata( $attachment_id, $attachment_data ); 113 114 // Relate to a post. 115 if ( ! empty( $post_id ) && (int) $post_id > 0 ) { 116 set_post_thumbnail( $post_id, $attachment_id ); 117 } 118 119 $meta = wp_get_attachment_metadata( $attachment_id ); 120 $post = get_post( $attachment_id ); 121 if ( is_array( $meta ) ) { 122 $response_data = array_merge( $meta, (array) $post ); 123 } else { 124 $response_data = (array) $post; 125 } 126 127 self::send_response( (object) array(), wp_json_encode( $response_data ) ); 128 } 129 130 131 private static function update_meta( $content_id, $content_type, $meta_fields ) { 321 322 // Skip updating meta for media and pages 323 if ( in_array( $content_type, array( 'media', 'pages' ), true ) ) { 324 return; 325 } 326 327 // Get appropriate functions for this content type 328 $functions = isset( $meta_functions[ $content_type ] ) ? $meta_functions[ $content_type ] : $meta_functions['default']; 329 132 330 foreach ( $meta_fields as $meta_key => $meta_value ) { 133 switch ( $content_type ) { 134 case 'media': 135 case 'pages': 136 return; 137 break; 138 case 'comments': 139 $function_update = 'update_comment_meta'; 140 $function_delete = 'delete_comment_meta'; 141 break; 142 case 'tags': 143 case 'categories': 144 $function_update = 'update_term_meta'; 145 $function_delete = 'delete_term_meta'; 146 break; 147 case 'users': 148 $function_update = 'update_user_meta'; 149 $function_delete = 'delete_user_meta'; 150 break; 151 default: 152 $function_update = 'update_post_meta'; 153 $function_delete = 'delete_post_meta'; 154 break; 155 } 156 157 switch ( $meta_value ) { 158 case 'IMT.REMOVE': 159 $function_delete( $content_id, $meta_key ); 160 break; 161 default: 162 $function_update( $content_id, $meta_key, $meta_value ); 163 break; 164 } 165 } 166 } 167 331 if ( $meta_value === 'IMT.REMOVE' ) { 332 $functions['delete']( $content_id, $meta_key ); 333 } else { 334 $functions['update']( $content_id, $meta_key, $meta_value ); 335 } 336 } 337 } 168 338 169 339 /** … … 180 350 } 181 351 } 182 183 352 } -
integromat-connector/trunk/class/class-rest-response.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Rest_Response { … … 62 64 * @param string $error_message 63 65 * @param string $error_code 66 * @param array $headers Optional headers to include 64 67 */ 65 public static function render_error( $status_code, $error_message, $error_code ) { 66 $out = '{ 67 "code": "' . $error_code . '", 68 "message": "' . $error_message . '", 69 "data": { 70 "status": ' . $status_code . ' 71 } 72 }'; 68 public static function render_error( $status_code, $error_message, $error_code, $headers = array(), $details = array() ) { 73 69 http_response_code( $status_code ); 74 70 header( 'Content-type: application/json' ); 75 // use wp_send_json_error instead? 76 die( trim( $out ) ); 71 72 $error = array( 73 "code" => $error_code, 74 "message" => $error_message, 75 "data" => array_merge( array ( "status" => $status_code ), $details ), 76 ); 77 78 foreach ( $headers as $name => $value ) { 79 $clean_name = preg_replace( '/[^\w-]/', '', $name ); 80 $clean_value = preg_replace( '/[\r\n]/', '', $value ); 81 if ( ! empty( $clean_name ) && ! empty( $clean_value ) ) { 82 header( "$clean_name: $clean_value" ); 83 } 84 } 85 86 echo wp_json_encode( $error ); 87 exit; 77 88 } 78 89 } -
integromat-connector/trunk/class/class-user.php
r2883308 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class User { … … 11 13 */ 12 14 public static function get_administrator_user() { 13 $users = get_users( array( 'role__in' => array( 'administrator') ) ); 15 $users = get_users( array( 'role__in' => array( 'administrator' ), 'number' => 5 ) ); 16 14 17 if ( empty( $users ) ) { 15 18 return 0; … … 18 21 // Prioritize user ID 1 (default admin). 19 22 foreach ( $users as $user ) { 20 if ( $user->data->ID == 1 && in_array( 'administrator', $user->roles ) ) {23 if ( $user->data->ID == 1 && in_array( 'administrator', $user->roles, true ) ) { 21 24 return $user->data->ID; 22 } ;25 } 23 26 } 24 27 25 28 // Search for another admin, if user ID 1 doesn't exist or hasn't administrator role. 26 29 foreach ( $users as $user ) { 27 if ( in_array( 'administrator', $user->roles ) ) {30 if ( in_array( 'administrator', $user->roles, true ) ) { 28 31 return $user->data->ID; 29 } ;32 } 30 33 } 31 34 … … 35 38 36 39 /** 37 * Log user in40 * Set current user context for API requests with specific permissions 38 41 * 39 42 * @param int $user_id 43 * @param string $endpoint The REST endpoint being accessed 44 * @param string $method HTTP method 45 * @return bool Success status 40 46 */ 41 public static function login( $user_id ) { 42 wp_clear_auth_cookie(); 47 public static function set_api_user_context( $user_id, $endpoint = '', $method = 'GET' ) { 48 // Validate user exists and has administrator role 49 $user = get_user_by( 'id', $user_id ); 50 if ( ! $user || ! in_array( 'administrator', $user->roles, true ) ) { 51 return false; 52 } 53 54 // Set user context without full authentication 43 55 wp_set_current_user( $user_id ); 56 57 // If endpoint and method provided, check API-specific permissions 58 // Note: Api_Permissions::check_permission() will internally verify if this is a Make request 59 if ( ! empty( $endpoint ) && ! empty( $method ) ) { 60 if ( ! \Integromat\Api_Permissions::check_permission( $endpoint, $method ) ) { 61 // Log permission failure 62 if ( get_option( 'iwc-logging-enabled' ) == 'true' ) { 63 \Integromat\Logger::write( 11 ); 64 } 65 return false; 66 } 67 } 68 69 return true; 44 70 } 45 71 -
integromat-connector/trunk/index.php
r3257687 r3361722 3 3 /** 4 4 * @package Integromat_Connector 5 * @version 1. 5.105 * @version 1.6.0 6 6 */ 7 7 … … 11 11 Author: Celonis s.r.o. 12 12 Author URI: https://www.make.com/en?utm_source=wordpress&utm_medium=partner&utm_campaign=wordpress-partner-make 13 Version: 1.5.10 13 Version: 1.6.0 14 License: GPL v2 or later 15 License URI: https://www.gnu.org/licenses/gpl-2.0.html 14 16 */ 15 17 … … 17 19 define('IWC_PLUGIN_NAME_SAFE', 'integromat-wordpress-connector'); 18 20 define('IWC_MENUITEM_IDENTIFIER', 'integromat_custom_fields'); 21 define('IWC_PLUGIN_VERSION', '1.6.0'); 19 22 20 23 require __DIR__ . '/class/class-user.php'; … … 24 27 require __DIR__ . '/class/class-guard.php'; 25 28 require __DIR__ . '/class/class-logger.php'; 29 require __DIR__ . '/class/class-api-permissions.php'; 30 require __DIR__ . '/class/class-rate-limiter.php'; 31 require __DIR__ . '/class/class-file-validator.php'; 26 32 27 33 require __DIR__ . '/api/authentication.php'; … … 34 40 $controller = new \Integromat\Controller(); 35 41 $controller->init(); 42 43 // Initialize API permissions 44 \Integromat\Api_Permissions::init(); 36 45 37 46 // Custom CSS, JS. … … 46 55 wp_enqueue_style( 47 56 'integromat_css', 48 plugin_dir_url(__FILE__) . 'assets/iwc.css' 57 plugin_dir_url(__FILE__) . 'assets/iwc.css', 58 [], 59 IWC_PLUGIN_VERSION 49 60 ); 50 61 wp_enqueue_script( 51 62 'integromat_js', 52 63 plugin_dir_url(__FILE__) . 'assets/iwc.js', 53 ['jquery-ui-tabs'] 64 ['jquery'], 65 IWC_PLUGIN_VERSION, 66 true 54 67 ); 68 69 // Localize script for AJAX 70 wp_localize_script('integromat_js', 'iwc_ajax', array( 71 'ajax_url' => admin_url('admin-ajax.php'), 72 'regenerate_nonce' => wp_create_nonce('iwc_regenerate_nonce'), 73 'purge_nonce' => wp_create_nonce('iwc_purge_nonce'), 74 'reveal_nonce' => wp_create_nonce('iwc_reveal_nonce') 75 )); 55 76 } 56 77 ); 78 79 // AJAX handler for API key regeneration 80 add_action('wp_ajax_iwc_regenerate_api_key', function() { 81 // Verify nonce 82 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'iwc_regenerate_nonce')) { 83 wp_send_json_error('Security check failed', 403); 84 return; 85 } 86 87 // Verify current user capabilities 88 if (!current_user_can('manage_options')) { 89 wp_send_json_error('Insufficient permissions', 403); 90 return; 91 } 92 93 // Verify confirmation text 94 $confirmation = sanitize_text_field(wp_unslash($_POST['confirmation'] ?? '')); 95 if (strtolower($confirmation) !== 'regenerate') { 96 wp_send_json_error('Confirmation text does not match'); 97 return; 98 } 99 100 try { 101 // Regenerate the API key 102 $new_token = \Integromat\Api_Token::regenerate(); 103 $masked_token = str_repeat('•', 20) . substr($new_token, -4); 104 105 wp_send_json_success(array( 106 'message' => 'API key regenerated successfully', 107 'new_token' => $new_token, 108 'masked_token' => $masked_token 109 )); 110 } catch (Exception $e) { 111 wp_send_json_error('Failed to regenerate API key: ' . $e->getMessage()); 112 } 113 }); 114 115 // AJAX handler for revealing API key 116 add_action('wp_ajax_iwc_reveal_api_key', function() { 117 // Verify nonce 118 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'iwc_reveal_nonce')) { 119 wp_send_json_error('Security check failed', 403); 120 return; 121 } 122 123 // Verify current user capabilities 124 if (!current_user_can('manage_options')) { 125 wp_send_json_error('Insufficient permissions', 403); 126 return; 127 } 128 129 try { 130 // Log the API key reveal action for security audit 131 if (class_exists('\\Integromat\\Logger')) { 132 $current_user = wp_get_current_user(); 133 \Integromat\Logger::write('API key revealed by user: ' . $current_user->user_login . ' (ID: ' . $current_user->ID . ')'); 134 } 135 136 // Get the current API key 137 $api_token = \Integromat\Api_Token::get(); 138 139 if (empty($api_token)) { 140 wp_send_json_error('No API key found'); 141 return; 142 } 143 144 wp_send_json_success(array( 145 'api_key' => $api_token 146 )); 147 } catch (Exception $e) { 148 wp_send_json_error('Failed to retrieve API key: ' . $e->getMessage()); 149 } 150 }); 151 152 // AJAX handler for log purging 153 add_action('wp_ajax_iwc_purge_logs', function() { 154 // Verify nonce 155 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'] ?? '')), 'iwc_purge_nonce')) { 156 wp_send_json_error('Security check failed', 403); 157 return; 158 } 159 160 // Verify current user capabilities 161 if (!current_user_can('manage_options')) { 162 wp_send_json_error('Insufficient permissions', 403); 163 return; 164 } 165 166 try { 167 // Purge the logs 168 $result = \Integromat\Logger::purge(); 169 170 if ($result) { 171 wp_send_json_success('All log data has been successfully purged'); 172 } else { 173 wp_send_json_error('Failed to purge log data'); 174 } 175 } catch (Exception $e) { 176 wp_send_json_error('Failed to purge logs: ' . $e->getMessage()); 177 } 178 }); 179 180 // Activation and deactivation hooks for API permissions 181 register_activation_hook( __FILE__, function() { 182 \Integromat\Api_Permissions::add_api_capabilities(); 183 iwc_set_default_settings(); 184 }); 185 186 register_deactivation_hook( __FILE__, function() { 187 \Integromat\Api_Permissions::remove_api_capabilities(); 188 iwc_cleanup_on_deactivation(); 189 }); 190 191 /** 192 * Cleanup when plugin is deactivated 193 */ 194 function iwc_cleanup_on_deactivation() { 195 // Remove version tracking 196 delete_option('iwc_plugin_version'); 197 198 // Note: We intentionally don't remove user settings or API tokens 199 // to preserve user configuration if they reactivate the plugin 200 } 201 202 /** 203 * Set default settings when plugin is activated 204 */ 205 function iwc_set_default_settings() { 206 // Check if this is a fresh installation or upgrade 207 $current_version = get_option('iwc_plugin_version'); 208 209 // Only set defaults on fresh installation 210 if (empty($current_version)) { 211 // General settings - logging disabled by default 212 add_option('iwc-logging-enabled', 'false'); 213 214 // API permissions - disabled by default 215 add_option('iwc_api_permissions_enabled', '0'); 216 217 // Individual API permissions - all disabled by default 218 $api_permissions = array( 219 'iwc_read_posts', 'iwc_create_posts', 'iwc_edit_posts', 'iwc_delete_posts', 220 'iwc_read_users', 'iwc_create_users', 'iwc_edit_users', 'iwc_delete_users', 221 'iwc_read_comments', 'iwc_create_comments', 'iwc_edit_comments', 'iwc_delete_comments', 222 'iwc_upload_files', 'iwc_read_media', 'iwc_edit_media', 'iwc_delete_media', 223 'iwc_read_terms', 'iwc_create_terms', 'iwc_edit_terms', 'iwc_delete_terms', 224 ); 225 226 foreach ($api_permissions as $permission) { 227 add_option('iwc_permission_' . $permission, '0'); 228 } 229 230 // Security settings - all disabled by default for backward compatibility 231 add_option('iwc_rate_limit_enabled', '0'); 232 add_option('iwc_rate_limit_requests', '100'); 233 add_option('iwc_payload_limit_enabled', '0'); 234 add_option('iwc_max_payload_size', '10'); 235 add_option('iwc_strict_file_validation', '0'); 236 add_option('iwc_allowed_file_extensions', 'jpg,jpeg,png,gif,webp,svg,bmp,ico,pdf,doc,docx,xls,xlsx,ppt,pptx,txt,rtf,odt,ods,zip,rar,7z,tar,gz,mp3,wav,mp4,avi,mov,wmv,flv,webm,json,xml,csv'); 237 add_option('iwc_log_security_events', '0'); 238 } 239 240 // Generate API token if it doesn't exist (always check this) 241 if (empty(\Integromat\Api_Token::get())) { 242 \Integromat\Api_Token::initiate(); 243 } 244 245 // Update plugin version 246 update_option('iwc_plugin_version', IWC_PLUGIN_VERSION); 247 } -
integromat-connector/trunk/readme.txt
r3355938 r3361722 3 3 Tags: make, integromat, rest, api, rest api 4 4 Requires at least: 5.0 5 Tested up to: 6.7.25 Tested up to: 6.8 6 6 Requires PHP: 7.2 7 Stable tag: 1. 5.107 Stable tag: 1.6.0 8 8 License: GPLv2 or later 9 9 … … 45 45 46 46 == Changelog == 47 = 1.6.0 = 48 * Security improvement: Granular API permissions 49 * Security improvement: Configurable rate limiting 50 * New feature: Enhanced file upload validation 51 * New feature: Request payload size limits 52 * New feature: API key rotation 53 * New feature: Purge log 54 * Fix multiple vulnerabilities 55 47 56 = 1.5.10 = 48 57 * Fix a bug introduced in previous fix regarding PHP 7 compatibility -
integromat-connector/trunk/settings/class-controller.php
r2883308 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Controller { … … 12 14 function () { 13 15 global $pagenow; 14 if ( 'options.php' === $pagenow || $pagenow === 'admin.php' && isset( $_GET['page'] ) && $_GET['page'] === IWC_MENUITEM_IDENTIFIER ) { 16 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page check, no form processing 17 if ( 'options.php' === $pagenow || ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && sanitize_text_field( wp_unslash( $_GET['page'] ) ) === IWC_MENUITEM_IDENTIFIER ) ) { 15 18 // Posts. 16 19 require_once __DIR__ . '/object-types/class-post-meta.php'; … … 34 37 } 35 38 36 if ( $pagenow == 'options.php' || $pagenow == 'admin.php' && isset( $_GET['page'] ) && $_GET['page'] == 'integromat_custom_toxonomies' ) { 39 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Admin page check, no form processing 40 if ( 'options.php' === $pagenow || ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && sanitize_text_field( wp_unslash( $_GET['page'] ) ) === 'integromat_custom_toxonomies' ) ) { 37 41 // Taxonomies. 38 42 require_once __DIR__ . '/object-types/custom-taxonomy.php'; … … 41 45 require_once __DIR__ . '/object-types/general.php'; 42 46 add_general_menu(); 47 48 // Security settings 49 require_once __DIR__ . '/object-types/security.php'; 50 add_security_menu(); 43 51 } 44 52 ); -
integromat-connector/trunk/settings/class-meta-object.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Meta_Object { … … 13 15 public function get_meta_items( $table ) { 14 16 global $wpdb; 15 $query = " 16 SELECT DISTINCT(meta_key) 17 FROM $table 18 ORDER BY meta_key 19 "; 20 return $wpdb->get_col( $query ); 17 18 // Validate table name against known WordPress meta tables for security 19 $allowed_tables = array( 20 $wpdb->postmeta, 21 $wpdb->usermeta, 22 $wpdb->commentmeta, 23 $wpdb->termmeta 24 ); 25 26 if ( ! in_array( $table, $allowed_tables, true ) ) { 27 return array(); 28 } 29 30 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- No WordPress function exists to get distinct meta keys from meta tables, one-time admin query, table name validated against whitelist 31 $query = "SELECT DISTINCT(meta_key) FROM `" . esc_sql( $table ) . "` ORDER BY meta_key"; 32 $result = $wpdb->get_col( $query ); 33 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared 34 35 return is_array( $result ) ? $result : array(); 21 36 } 22 37 -
integromat-connector/trunk/settings/events.php
r2785241 r3361722 6 6 // Download a log file. 7 7 if ( isset( $_GET['iwcdlogf'] ) ) { 8 if ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( $_REQUEST['_wpnonce'], 'log-nonce' ) ) {8 if ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'log-nonce' ) ) { 9 9 \Integromat\Logger::download(); 10 10 } else { 11 die( __( 'Wrong nonce', 'textdomain') );11 wp_die( esc_html__( 'Security check failed', 'integromat-connector' ), esc_html__( 'Error', 'integromat-connector' ), array( 'response' => 403 ) ); 12 12 } 13 13 } -
integromat-connector/trunk/settings/object-types/class-comments-meta.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Comments_Meta extends Meta_Object { … … 12 14 global $wpdb; 13 15 $this->meta_item_keys = $this->get_meta_items($wpdb->commentmeta); 14 register_setting('integromat_api_comment', 'integromat_api_options_comment'); 16 register_setting('integromat_api_comment', 'integromat_api_options_comment', array( 17 'sanitize_callback' => array( $this, 'sanitize_comment_options' ), 18 )); 15 19 16 20 add_settings_section( 17 21 'integromat_api_section_comments', 18 __(' ', 'integromat_api_comment'),22 __('Comments Metadata Settings', 'integromat-connector'), 19 23 function () { 20 24 ?> 21 <p><?php esc_html_e('Select comments metadata to include in REST API response', 'integromat _api_comment'); ?></p>25 <p><?php esc_html_e('Select comments metadata to include in REST API response', 'integromat-connector'); ?></p> 22 26 <p><a class="uncheck_all" data-status="0">Un/check all</a></p> 23 27 <?php … … 30 34 IWC_FIELD_PREFIX . $meta_item, 31 35 32 __($meta_item, 'integromat_api_comment'),36 esc_html($meta_item), 33 37 function ($args) use($meta_item) { 34 38 $options = get_option('integromat_api_options_comment'); … … 51 55 } 52 56 57 /** 58 * Sanitize comment options 59 * 60 * @param array $input 61 * @return array 62 */ 63 public function sanitize_comment_options( $input ) { 64 if ( ! is_array( $input ) ) { 65 return array(); 66 } 67 68 $sanitized = array(); 69 foreach ( $input as $key => $value ) { 70 $sanitized[ sanitize_key( $key ) ] = sanitize_text_field( $value ); 71 } 72 73 return $sanitized; 74 } 75 53 76 } 54 -
integromat-connector/trunk/settings/object-types/class-post-meta.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Posts_Meta extends Meta_Object { … … 12 14 public function init() { 13 15 $this->meta_item_keys = $this->get_post_meta_items(); 14 register_setting( 'integromat_api_post', 'integromat_api_options_post' ); 16 register_setting( 'integromat_api_post', 'integromat_api_options_post', array( 17 'sanitize_callback' => array( $this, 'sanitize_post_options' ), 18 ) ); 15 19 16 20 add_settings_section( 17 21 'integromat_api_section_posts', 18 __( ' ', 'integromat_api_post' ), // h1 title as the first argument.22 __( 'Posts Metadata Settings', 'integromat-connector' ), // h1 title as the first argument. 19 23 function () { 20 24 ?> 21 <p><?php esc_html_e( 'Select posts metadata to include in REST API response', 'integromat _api_post' ); ?></p>25 <p><?php esc_html_e( 'Select posts metadata to include in REST API response', 'integromat-connector' ); ?></p> 22 26 <p><a class="uncheck_all" data-status="0">Un/check all</a></p> 23 27 <?php … … 71 75 add_settings_field( 72 76 IWC_FIELD_PREFIX . $meta_item, 73 __( $meta_item, 'integromat_api_post'),77 esc_html( $meta_item ), 74 78 function ( $args ) use ( $meta_item, $object_type, $last_object_type ) { 75 79 $options = get_option( 'integromat_api_options_post' ); … … 116 120 add_settings_field( 117 121 IWC_FIELD_PREFIX . $meta_item, 118 __( $meta_item, 'integromat_api_post'),122 esc_html( $meta_item ), 119 123 function ( $args ) use ( $meta_item ) { 120 124 $options = get_option( 'integromat_api_options_post' ); … … 146 150 public function get_post_meta_items() { 147 151 global $wpdb; 148 $query = ' 149 SELECT 150 DISTINCT(m.meta_key), 151 p.post_type 152 FROM ' . $wpdb->base_prefix . 'postmeta m 153 INNER JOIN ' . $wpdb->base_prefix . 'posts p ON p.ID = m.post_id 154 '; 152 153 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- No WordPress function exists for JOIN queries to get distinct meta keys with post types, one-time admin query, table names validated 154 $query = "SELECT DISTINCT(m.meta_key), p.post_type 155 FROM `" . esc_sql( $wpdb->postmeta ) . "` m 156 INNER JOIN `" . esc_sql( $wpdb->posts ) . "` p ON p.ID = m.post_id"; 155 157 $meta_keys = $wpdb->get_results( $query ); 156 return $meta_keys; 158 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared 159 160 return is_array( $meta_keys ) ? $meta_keys : array(); 157 161 } 162 163 /** 164 * Sanitize post options 165 * 166 * @param array $input 167 * @return array 168 */ 169 public function sanitize_post_options( $input ) { 170 if ( ! is_array( $input ) ) { 171 return array(); 172 } 173 174 $sanitized = array(); 175 foreach ( $input as $key => $value ) { 176 $sanitized[ sanitize_key( $key ) ] = sanitize_text_field( $value ); 177 } 178 179 return $sanitized; 180 } 181 158 182 } -
integromat-connector/trunk/settings/object-types/class-term-meta.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Terms_Meta extends Meta_Object { … … 8 10 global $wpdb; 9 11 $this->meta_item_keys = $this->get_meta_items( $wpdb->termmeta ); 10 register_setting( 'integromat_api_term', 'integromat_api_options_term' ); 12 register_setting( 'integromat_api_term', 'integromat_api_options_term', array( 13 'sanitize_callback' => array( $this, 'sanitize_term_options' ), 14 ) ); 11 15 12 16 add_settings_section( 13 17 'integromat_api_section_terms', 14 __( ' ', 'integromat_api_term' ),18 __( 'Terms Metadata Settings', 'integromat-connector' ), 15 19 function () { 16 20 ?> 17 <p><?php esc_html_e( 'Select terms metadata to include in REST API response', 'integromat _api_term' ); ?></p>21 <p><?php esc_html_e( 'Select terms metadata to include in REST API response', 'integromat-connector' ); ?></p> 18 22 <p><a class="uncheck_all" data-status="0">Un/check all</a></p> 19 23 <?php … … 25 29 add_settings_field( 26 30 IWC_FIELD_PREFIX . $meta_item, 27 __( $meta_item, 'integromat_api_term'),31 esc_html( $meta_item ), 28 32 function ( $args ) use ( $meta_item ) { 29 33 $options = get_option( 'integromat_api_options_term' ); … … 45 49 } 46 50 } 51 52 /** 53 * Sanitize term options 54 * 55 * @param array $input 56 * @return array 57 */ 58 public function sanitize_term_options( $input ) { 59 if ( ! is_array( $input ) ) { 60 return array(); 61 } 62 63 $sanitized = array(); 64 foreach ( $input as $key => $value ) { 65 $sanitized[ sanitize_key( $key ) ] = sanitize_text_field( $value ); 66 } 67 68 return $sanitized; 69 } 47 70 } -
integromat-connector/trunk/settings/object-types/class-user-meta.php
r2783423 r3361722 2 2 3 3 namespace Integromat; 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 4 6 5 7 class Users_Meta extends Meta_Object { … … 12 14 global $wpdb; 13 15 $this->meta_item_keys = $this->get_meta_items( $wpdb->usermeta ); 14 register_setting( 'integromat_api_user', 'integromat_api_options_user' ); 16 register_setting( 'integromat_api_user', 'integromat_api_options_user', array( 17 'sanitize_callback' => array( $this, 'sanitize_user_options' ), 18 ) ); 15 19 16 20 add_settings_section( 17 21 'integromat_api_section_users', 18 __( ' ', 'integromat_api_user' ),22 __( 'Users Metadata Settings', 'integromat-connector' ), 19 23 function () { 20 24 ?> 21 <p><?php esc_html_e( 'Select users metadata to include in REST API response', 'integromat _api_user' ); ?></p>25 <p><?php esc_html_e( 'Select users metadata to include in REST API response', 'integromat-connector' ); ?></p> 22 26 <p><a class="uncheck_all" data-status="0">Un/check all</a></p> 23 27 <?php … … 29 33 add_settings_field( 30 34 IWC_FIELD_PREFIX . $meta_item, 31 __( $meta_item, 'integromat_api_user'),35 esc_html( $meta_item ), 32 36 function ( $args ) use ( $meta_item ) { 33 37 $options = get_option( 'integromat_api_options_user' ); … … 50 54 } 51 55 56 /** 57 * Sanitize user options 58 * 59 * @param array $input 60 * @return array 61 */ 62 public function sanitize_user_options( $input ) { 63 if ( ! is_array( $input ) ) { 64 return array(); 65 } 66 67 $sanitized = array(); 68 foreach ( $input as $key => $value ) { 69 $sanitized[ sanitize_key( $key ) ] = sanitize_text_field( $value ); 70 } 71 72 return $sanitized; 73 } 74 52 75 } 53 76 -
integromat-connector/trunk/settings/object-types/custom-taxonomy.php
r2883308 r3361722 3 3 namespace Integromat; 4 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 6 5 7 function add_taxonomies() { 6 register_setting( 'integromat_api_taxonomy', 'integromat_api_options_taxonomy' ); 8 register_setting( 'integromat_api_taxonomy', 'integromat_api_options_taxonomy', array( 9 'sanitize_callback' => __NAMESPACE__ . '\sanitize_taxonomy_options', 10 ) ); 7 11 8 12 add_settings_section( … … 11 15 function () { 12 16 ?> 13 <p><?php esc_html_e( 'Select taxonomies to enable or disable in REST API response.', 'integromat _api_post' ); ?></p>17 <p><?php esc_html_e( 'Select taxonomies to enable or disable in REST API response.', 'integromat-connector' ); ?></p> 14 18 <p><a class="uncheck_all" data-status="0">Un/check all</a></p> 15 19 <?php … … 42 46 } 43 47 } 48 49 /** 50 * Sanitize taxonomy options 51 * 52 * @param array $input 53 * @return array 54 */ 55 function sanitize_taxonomy_options( $input ) { 56 if ( ! is_array( $input ) ) { 57 return array(); 58 } 59 60 $sanitized = array(); 61 foreach ( $input as $key => $value ) { 62 $sanitized[ sanitize_key( $key ) ] = sanitize_text_field( $value ); 63 } 64 65 return $sanitized; 66 } -
integromat-connector/trunk/settings/object-types/general.php
r2785241 r3361722 1 1 <?php 2 2 3 namespace Integromat; 3 4 5 defined( 'ABSPATH' ) || die( 'No direct access allowed' ); 6 4 7 function add_general_menu() { 5 register_setting( 'integromat_main', 'iwc-logging-enabled' ); // register the same name in settings as before to pick it up in old installations. 8 register_setting( 'integromat_main', 'iwc-logging-enabled', array( 9 'sanitize_callback' => 'sanitize_text_field', 10 ) ); // register the same name in settings as before to pick it up in old installations. 11 12 // Register API permissions settings in main group for general page 13 register_setting( 'integromat_main', 'iwc_api_permissions_enabled', array( 14 'sanitize_callback' => 'sanitize_text_field', 15 'default' => '0', 16 ) ); 17 18 // Register individual API permission settings in main group 19 $api_permissions = array( 20 'iwc_read_posts', 'iwc_create_posts', 'iwc_edit_posts', 'iwc_delete_posts', 21 'iwc_read_users', 'iwc_create_users', 'iwc_edit_users', 'iwc_delete_users', 22 'iwc_read_comments', 'iwc_create_comments', 'iwc_edit_comments', 'iwc_delete_comments', 23 'iwc_upload_files', 'iwc_read_media', 'iwc_edit_media', 'iwc_delete_media', 24 'iwc_read_terms', 'iwc_create_terms', 'iwc_edit_terms', 'iwc_delete_terms', 25 ); 26 27 foreach ($api_permissions as $permission) { 28 register_setting( 'integromat_main', 'iwc_permission_' . $permission, array( 29 'sanitize_callback' => 'sanitize_text_field', 30 'default' => '0', 31 ) ); 32 } 6 33 7 34 add_settings_section( … … 18 45 function ( $args ) { 19 46 $api_token = $args['api_key']; 20 ?> 21 <input type="text" 22 id="iwc-api-key-value" 23 readonly="readonly" 24 value="<?php echo esc_attr( $api_token ); ?>" 25 class="w-300"> 47 $masked_token = str_repeat('•', 20) . substr($api_token, -4); // Show last 4 characters 48 ?> 49 <div class="iwc-api-key-container"> 50 <input type="text" 51 id="iwc-api-key-value" 52 readonly="readonly" 53 value="<?php echo esc_attr( $masked_token ); ?>" 54 data-masked="<?php echo esc_attr( $masked_token ); ?>" 55 class="w-300"> 56 <button type="button" 57 id="iwc-api-key-toggle" 58 class="button" 59 data-state="masked"> 60 Reveal 61 </button> 62 <button type="button" 63 id="iwc-api-key-regenerate" 64 class="button iwc-confirm-btn" 65 title="Generate a new API key (this will break existing connections)"> 66 Regenerate 67 </button> 68 </div> 26 69 <p class="comment">Use this token when creating a new connection in the WordPress app.</p> 27 70 <?php … … 32 75 'api_key' => \Integromat\Api_Token::get(), 33 76 ) 77 ); 78 79 add_settings_field( 80 'api_permissions_control', 81 'API Permissions', 82 function ( $args ) { 83 $api_permissions_enabled = get_option( 'iwc_api_permissions_enabled', '0' ); 84 ?> 85 <div class="iwc-api-permissions-container"> 86 <label> 87 <input type="checkbox" name="iwc_api_permissions_enabled" value="1" <?php checked( $api_permissions_enabled, '1' ); ?> /> 88 Enable granular API permissions (recommended) 89 </label> 90 91 <div class="notice notice-info" style="margin: 10px 0; padding: 10px; background: #e7f3ff; border: 1px solid #72aee6; border-left: 4px solid #0073aa;"> 92 <p style="margin: 0; font-size: 13px;"> 93 <strong>ℹ️ Important:</strong> If you are using the "Make an API Call" module in your scenarios, DO NOT enable granular permissions. 94 </p> 95 </div> 96 97 <div class="notice notice-warning" style="margin: 10px 0; padding: 10px; background: #fff3cd; border: 1px solid #ffeaa7; border-left: 4px solid #ffb900;"> 98 <p style="margin: 0; font-size: 13px;"> 99 <strong>⚠️ Warning:</strong> Changing API permissions may break existing Make scenarios. 100 Test your scenarios after making changes and ensure required permissions are enabled for your integrations to work properly. 101 </p> 102 </div> 103 104 <div id="iwc-permissions-details" style="margin-top: 15px; <?php echo $api_permissions_enabled === '1' ? '' : 'display:none;'; ?>"> 105 <div style="margin-bottom: 10px;"> 106 <button type="button" class="button button-small iwc-perm-enable-all">All</button> 107 <button type="button" class="button button-small iwc-perm-disable-all">None</button> 108 </div> 109 110 <div class="iwc-permissions-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; border: 1px solid #ddd; padding: 15px; background: #f9f9f9;"> 111 <?php 112 $permission_groups = array( 113 'Posts' => array('read', 'create', 'update', 'delete'), 114 'Users' => array('read', 'create', 'update', 'delete'), 115 'Comments' => array('read', 'create', 'update', 'delete'), 116 'Media' => array('read', 'upload', 'update', 'delete'), 117 'Terms' => array('read', 'create', 'update', 'delete'), 118 ); 119 120 foreach ($permission_groups as $group_name => $operations) { 121 echo '<div class="iwc-permission-group">'; 122 echo '<h4 style="margin: 0 0 8px 0; font-size: 13px;">' . esc_html($group_name) . '</h4>'; 123 124 foreach ($operations as $operation) { 125 // Map display name to actual capability name 126 $capability_operation = ($operation === 'update') ? 'edit' : $operation; 127 $capability = ($group_name === 'Media' && $operation === 'upload') ? 'iwc_upload_files' : 'iwc_' . $capability_operation . '_' . strtolower($group_name); 128 129 $permission_enabled = get_option('iwc_permission_' . $capability, '0'); 130 $is_dangerous = in_array($operation, array('delete', 'upload')); 131 $color = $is_dangerous ? 'color: #d63384;' : ''; 132 133 echo '<label style="display: block; margin-bottom: 4px; font-size: 12px; ' . esc_attr($color) . '">'; 134 echo '<input type="checkbox" name="iwc_permission_' . esc_attr($capability) . '" value="1" ' . checked($permission_enabled, '1', false) . ' style="margin-right: 5px;" />'; 135 echo esc_html(ucfirst($operation)); 136 echo '</label>'; 137 } 138 echo '</div>'; 139 } 140 ?> 141 </div> 142 143 <p class="description" style="margin-top: 10px;"> 144 <strong style="color: #d63384;">Dangerous permissions:</strong> Delete and upload operations. 145 </p> 146 </div> 147 </div> 148 149 <script> 150 jQuery(document).ready(function($) { 151 // Toggle permissions detail visibility 152 $('input[name="iwc_api_permissions_enabled"]').change(function() { 153 if ($(this).is(':checked')) { 154 $('#iwc-permissions-details').show(); 155 } else { 156 $('#iwc-permissions-details').hide(); 157 } 158 }); 159 160 // Bulk controls 161 $('.iwc-perm-enable-all').click(function() { 162 $('.iwc-permissions-grid input[type="checkbox"]').prop('checked', true); 163 }); 164 165 $('.iwc-perm-disable-all').click(function() { 166 $('.iwc-permissions-grid input[type="checkbox"]').prop('checked', false); 167 }); 168 }); 169 </script> 170 <?php 171 }, 172 'integromat_main', 173 'integromat_main_section', 174 array() 34 175 ); 35 176 … … 63 204 $href = $args['enabled'] ? "href={$url}&_wpnonce={$nonce}" : ''; 64 205 ?> 65 <a class="button <?php echo esc_attr( $enabled ); ?>" <?php echo esc_url( $href ); ?> >Download</a> 206 <div class="iwc-log-actions"> 207 <a class="button <?php echo esc_attr( $enabled ); ?>" <?php echo esc_url( $href ); ?> >Download</a> 208 <button type="button" 209 id="iwc-log-purge" 210 class="button iwc-confirm-btn <?php echo esc_attr( $enabled ); ?>" 211 <?php echo $args['enabled'] ? '' : 'disabled'; ?> 212 title="Delete all stored log data"> 213 Purge 214 </button> 215 </div> 66 216 <p class="iwc-comment "> 67 Although we try to remove them, there could still be some potentially sensitive information (like authentication tokens or passwords) contained in the file.68 Please check the section between =SERVER INFO START= and =SERVER INFO END= delimiters (located at the start of thefile) and possibly remove the sensitive data (or whole section) before sending this file to someone else.217 Although we try to remove them, there could still be some potentially sensitive information (like authentication tokens or passwords) contained in the downloaded file. 218 The downloaded file includes server information and CSV headers that are generated at download time. Please check the section between =SERVER INFO START= and =SERVER INFO END= delimiters (located at the start of the downloaded file) and possibly remove the sensitive data (or whole section) before sending this file to someone else. 69 219 </p> 70 220 <?php -
integromat-connector/trunk/settings/render.php
r2911623 r3361722 5 5 function () { 6 6 add_menu_page( 7 ' Make',7 'General Settings', 8 8 'Make', 9 9 'manage_options', … … 18 18 }, 19 19 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pgo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIKICJodHRwOi8vd3d3LnczLm9yZy9UUi8yMDAxL1JFQy1TVkctMjAwMTA5MDQvRFREL3N2ZzEwLmR0ZCI+CjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiB3aWR0aD0iNTEyLjAwMDAwMHB0IiBoZWlnaHQ9IjUxMi4wMDAwMDBwdCIgdmlld0JveD0iMCAwIDUxMi4wMDAwMDAgNTEyLjAwMDAwMCIKIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiPgoKPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMC4wMDAwMDAsNTEyLjAwMDAwMCkgc2NhbGUoMC4xMDAwMDAsLTAuMTAwMDAwKSIKZmlsbD0iIzAwMDAwMCIgc3Ryb2tlPSJub25lIj4KPHBhdGggZD0iTTAgMjU2MCBsMCAtMjU2MCAyNTYwIDAgMjU2MCAwIDAgMjU2MCAwIDI1NjAgLTI1NjAgMCAtMjU2MCAwIDAKLTI1NjB6IG0yNzkwIDEwMDUgYzI2OSAtNTQgMzAzIC02NSAzMTQgLTEwNCAzIC05IC03OSAtNDQ1IC0xODMgLTk2OSAtMTQwCi03MDkgLTE5MyAtOTU3IC0yMDYgLTk3MiAtMTAgLTExIC0zMCAtMjAgLTQ0IC0yMCAtNDcgMCAtNTM2IDEwMSAtNTU4IDExNgotMTMgOCAtMjUgMjQgLTI4IDM3IC0zIDEyIDgwIDQ1MSAxODMgOTc2IDE3MCA4NTYgMTkxIDk1NiAyMTIgOTczIDEyIDEwIDI1CjE4IDI5IDE4IDQgMCAxMzAgLTI1IDI4MSAtNTV6IG0tNzIzIC04OSBjMjIwIC0xMTAgMjYzIC0xMzggMjYzIC0xNzYgMCAtMjQKLTg3MCAtMTc1MyAtODkyIC0xNzcyIC0xMSAtMTAgLTMwIC0xOCAtNDIgLTE4IC0yMyAwIC00ODcgMjMxIC01MTggMjU4IC0xMCA4Ci0xOCAyOSAtMTggNDUgMCAyMCAxNTAgMzI4IDQzNiA4OTYgMjQwIDQ3NiA0NDAgODcxIDQ0NiA4NzggMTIgMTUgNDcgMjEgNzQKMTIgMTEgLTQgMTI0IC01OSAyNTEgLTEyM3ogbTE3NTggODkgbDI1IC0yNCAwIC05NzUgYzAgLTY5MyAtMyAtOTgyIC0xMSAtOTk5Ci0yMCAtNDQgLTQ0IC00NyAtMzI1IC00NyAtMjg2IDAgLTMxNSA1IC0zMjggNTIgLTMgMTMgLTYgNDYxIC02IDk5NiBsMCA5NzMKMjUgMjQgMjQgMjUgMjg2IDAgMjg2IDAgMjQgLTI1eiIvPgo8L2c+Cjwvc3ZnPg==' 20 // 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+DQo8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDIwMDEwOTA0Ly9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSLzIwMDEvUkVDLVNWRy0yMDAxMDkwNC9EVEQvc3ZnMTAuZHRkIj4NCjxzdmcgdmVyc2lvbj0iMS4wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDI0LjAwMDAwMHB0IiBoZWlnaHQ9IjEwMjQuMDAwMDAwcHQiIHZpZXdCb3g9IjAgMCAxMDI0LjAwMDAwMCAxMDI0LjAwMDAwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgbWVldCI+DQo8bWV0YWRhdGE+DQpDcmVhdGVkIGJ5IHBvdHJhY2UgMS4xMSwgd3JpdHRlbiBieSBQZXRlciBTZWxpbmdlciAyMDAxLTIwMTMNCjwvbWV0YWRhdGE+DQo8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLjAwMDAwMCwxMDI0LjAwMDAwMCkgc2NhbGUoMC4xMDAwMDAsLTAuMTAwMDAwKSIgZmlsbD0iI0ZGRkZGRiIgc3Ryb2tlPSJub25lIj4NCjxwYXRoIGQ9Ik00ODU4IDEwMjM1IGMtMiAtMiAtNTAgLTYgLTEwOCAtOSAtNTggLTQgLTExNiAtOCAtMTMwIC0xMSAtMTQgLTIgLTUyIC02IC04NSAtOSAtNjEgLTYgLTc4IC04IC0xOTcgLTI3IGwtNjggLTExIC0xIC0zNiBjMCAtMzkgMCAtMTU5NyAxIC0xNjY4IGwwIC00MSA1MyAxNCBjNTUgMTQgMjk0IDU5IDM2MiA2NyAxMjcgMTYgMjE0IDIwIDQzMCAyMSAyMzkgMCAzNjMgLTcgNDkwIC0yOCAxNyAtMyA1MCAtOSA3NSAtMTIgNTkgLTEwIDU5IC05IDE3OCAtMzUgNTcgLTEyIDEwNiAtMjAgMTA5IC0xNiA3IDcgMTAgMTczNiAyIDE3MzYgLTMgMCAtNTIgNyAtMTEwIDE2IC01NyA5IC0xMjMgMTcgLTE0NiAxOSAtMjMgMiAtNjUgNyAtOTUgMTAgLTI5IDQgLTc1IDkgLTEwMyAxMSAtNjMgNSAtNjUyIDEzIC02NTcgOXoiLz4NCjxwYXRoIGQ9Ik0zMzUwIDk5MjQgYy0zNiAtMTQgLTc3IC0yOSAtOTEgLTM0IC0xNSAtNCAtNDIgLTE1IC02MCAtMjMgLTE5IC04IC04MyAtMzYgLTE0NCAtNjMgLTE2NCAtNzEgLTQ0NiAtMjE4IC01OTAgLTMwNyAtNDAxIC0yNDkgLTcwMiAtNDkwIC0xMDEwIC04MDcgLTEyNiAtMTI5IC0xNjkgLTE3NiAtMjA5IC0yMjUgLTIwIC0yNCAtNDMgLTUwIC00OSAtNTcgLTQzIC00NCAtMjQ0IC0zMTIgLTMyNiAtNDM2IC0xMTQgLTE3MCAtMTU2IC0yMzggLTI1NCAtNDE3IC0xMDggLTE5NyAtMjQyIC00OTUgLTMxMyAtNjk3IC0xNCAtNDAgLTI4IC04MCAtMzEgLTg4IC0zIC04IC03IC0xOSAtOSAtMjUgLTIgLTUgLTE3IC01NSAtMzQgLTExMCAtOTMgLTI5NiAtMTcxIC02NjkgLTE5NiAtOTM1IC0yIC0zMCAtNyAtNjQgLTkgLTc1IC0xMSAtNTQgLTIwIC0yNzUgLTE5IC01MTAgMCAtMjczIDcgLTQwNyAyOSAtNTg1IDIgLTE5IDcgLTYwIDExIC05MCA5IC04NCA2MSAtMzY5IDkwIC00ODkgMTk0IC04MjYgNTczIC0xNTU4IDExNDkgLTIyMjAgNzggLTkwIDI3MSAtMjg2IDM4OSAtMzk2IDE4MCAtMTY3IDQ4MCAtMzk2IDcwOSAtNTQxIDMzNCAtMjEyIDY3MSAtMzczIDEwNjcgLTUxMyAxMDYgLTM4IDM1NyAtMTEzIDQxNyAtMTI2IDEwIC0yIDU2IC0xMyAxMDMgLTI0IDEyMCAtMjggMzQ0IC02OCA0ODAgLTg2IDI1MyAtMzIgMzY0IC0zOSA2NzAgLTM5IDIzOCAwIDQwOCA2IDUwMCAxOSAxNCAyIDUyIDYgODUgMTAgNjEgNiA3NSA4IDE1NSAyMCA5MCAxMyAxMDcgMTYgMjE1IDM2IDEyMiAyMyAyOTMgNjEgMzcwIDgzIDE2MiA0NyAyMDcgNjAgMjc1IDgzIDgwNCAyNjcgMTUwOCA3MTIgMjA5NSAxMzIzIDY4IDcxIDk2IDEwMSAxOTIgMjEwIDMxIDM1IDE1OSAxOTQgMjA2IDI1NSAyNDggMzI2IDUwNyA3ODcgNjY3IDExOTAgODQgMjExIDE4OSA1NDggMjI2IDcyNCA4IDQxIDE3IDc3IDE5IDgxIDQgNiAxMyA1MSAyMCA5NSAxIDExIDEyIDc0IDIzIDE0MCAxMSA2NiAyNiAxNjMgMzIgMjE1IDYgNTIgMTMgMTA5IDE1IDEyNSAyNiAyMDQgMjYgODA1IDAgMTAxMCAtMiAxNyAtNiA1MyAtOSA4MCAtNSA0NiAtMTYgMTI0IC0zMSAyMzAgLTE2IDEwOSAtNDggMjYxIC05MiA0NDAgLTE3NyA3MjIgLTU0OCAxNDQ2IC0xMDM5IDIwMzAgLTE0NiAxNzQgLTM0MyAzNzkgLTQ4NSA1MDUgLTQwIDM2IC04MCA3MiAtODggODAgLTkgOCAtMzkgMzMgLTY2IDU1IC0yOCAyMiAtNTIgNDIgLTU1IDQ1IC02MSA2MSAtMzY2IDI3NyAtNTc1IDQwNyAtOTQgNTggLTI4NyAxNjMgLTQxNSAyMjYgLTIzNiAxMTUgLTUxMyAyMjcgLTUyNyAyMTMgLTEyIC0xMiAtOSAtMTg0NyAyIC0xODYxIDYgLTcgNTQgLTQwIDEwOCAtNzQgMjk3IC0xODUgNTk5IC00NTQgODI3IC03MzYgMzU2IC00MzkgNjE0IC05OTkgNzA2IC0xNTM1IDQgLTI1IDggLTQ3IDkgLTUwIDEgLTMgNSAtMzIgOSAtNjUgNCAtMzIgOSAtNjcgMTEgLTc2IDI2IC0xMzMgMjYgLTc5NyAwIC04MzggLTIgLTQgLTYgLTM1IC05IC02OSAtMyAtMzUgLTggLTY3IC0xMCAtNzAgLTIgLTQgLTcgLTI3IC0xMCAtNTIgLTMgLTI1IC04IC01NCAtMTEgLTY1IC03IC0yOCAtMzYgLTE1NiAtNDAgLTE3OSAtMTEgLTQ4IC0xMDEgLTMyMCAtMTM2IC00MDggLTI2MiAtNjU3IC03MjkgLTEyMjMgLTEzMjkgLTE2MDkgLTk4IC02NCAtMjI1IC0xMzcgLTI5MCAtMTY4IC0yNSAtMTIgLTUyIC0yNSAtNjAgLTMwIC01NCAtMzIgLTMzNSAtMTQ3IC0zODIgLTE1NiAtOSAtMiAtMjUgLTggLTM1IC0xMyAtNTEgLTI4IC00MzUgLTExOSAtNTc4IC0xMzggLTI3IC0zIC02MSAtOCAtNzUgLTEwIC0yMDYgLTI3IC02NzcgLTI3IC04NDAgMCAtMTQgMyAtNDcgNyAtNzUgMTEgLTE1MiAxOSAtNTE1IDEwNiAtNTY3IDEzNSAtMTAgNiAtMjQgMTAgLTMxIDEwIC0yNCAwIC0yNjQgOTggLTM5MCAxNTkgLTI3MCAxMzAgLTUxNiAyOTIgLTc0OCA0OTEgLTgzIDcxIC0yODEgMjY5IC0zNDQgMzQ0IC0yOCAzMiAtNTUgNjQgLTYwIDcwIC0xMDEgMTEwIC0zMDQgNDIxIC00MTIgNjMxIC03NCAxNDQgLTE4MiA0MTQgLTIyMCA1NTAgLTMxIDEwOSAtNjIgMjMzIC03MiAyODUgLTYgMzAgLTE0IDY2IC0xNiA4MCAtNyAzMiAtMjMgMTQzIC0zMCAxOTUgLTI4IDIyNSAtMjggNjYwIDEgODUwIDIgMTcgNiA0NiA4IDY1IDU4IDQ0MiAyNDkgOTUyIDUxMSAxMzYwIDQ3IDc0IDE5OCAyODEgMjI2IDMxMCA4IDggMzYgNDIgNjQgNzUgNzEgODYgMjc3IDI4OSAzNzYgMzcxIDE0NSAxMjEgMjM5IDE4OCA0NTkgMzI4IGwzNCAyMyAxIDkzNiBjMCA1MTYgMCA5MzcgMCA5MzYgMCAwIC0yOSAtMTEgLTY1IC0yNXoiLz4NCjxwYXRoIGQ9Ik00OTY4IDc2NzUgYy0yIC0xIC00MCAtNSAtODQgLTkgLTQ1IC0zIC04OCAtOCAtOTUgLTEwIC04IC0zIC0zNyAtNyAtNjYgLTEwIC0yOCAtMyAtODAgLTEyIC0xMTUgLTIwIC0zNSAtOCAtNzUgLTE3IC05MCAtMjAgLTI3IC00IC0yMzkgLTY3IC0yNDcgLTcyIC0yIC0yIC00IC0zOTE5IC0yIC0zOTcwIDEgLTcgMTEgLTE0IDI0IC0xNyAxMiAtMyA2MCAtMTcgMTA2IC0zMSAxMTIgLTM1IDI2NCAtNjcgMzg2IC04MSAyOCAtNCA2MSAtOSA3NSAtMTIgNTYgLTEyIDQ1MyAtOCA1NTIgNSAxNDcgMTkgMzI4IDU3IDQ1MyA5NCBsMTA3IDMzIDAgMTk1NSBjLTEgMTA3NSAtMSAxOTcxIC0xIDE5OTEgbC0xIDM1IC0xMjcgMzcgYy03MSAxOSAtMTY0IDQzIC0yMDggNTIgLTQ0IDkgLTg3IDE4IC05NSAyMCAtOCAxIC00NCA2IC04MCAxMCAtMzYgNCAtNzQgOSAtODUgMTIgLTIxIDQgLTQwMyAxMiAtNDA3IDh6Ii8+DQo8L2c+DQo8L3N2Zz4=' 20 ); 21 22 // Override the default submenu item to change "Make" to "General" 23 add_submenu_page( 24 'integromat', 25 'General Settings', 26 'General', 27 'manage_options', 28 'integromat', 29 function () { 30 if ( ! current_user_can( 'manage_options' ) ) { 31 return; 32 } 33 settings_errors( 'integromat_api_messages' ); 34 35 include_once __DIR__ . '/template/general_menu.phtml'; 36 } 21 37 ); 22 38 … … 51 67 } 52 68 ); 69 70 // Security settings page 71 add_submenu_page( 72 'integromat', 73 'Security Settings', 74 'Security', 75 'manage_options', 76 'integromat_security', 77 function () { 78 if ( ! current_user_can( 'manage_options' ) ) { 79 return; 80 } 81 settings_errors( 'integromat_security_messages' ); 82 include_once __DIR__ . '/template/security_settings.phtml'; 83 } 84 ); 53 85 } 54 86 ); -
integromat-connector/trunk/settings/template/customFields.phtml
r2529734 r3361722 1 <?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?> 1 2 <h1><?php echo esc_html(get_admin_page_title()); ?></h1> 2 3 3 <div class="imapie_settings_container">4 < div id="imapie_tabs">5 <ul>6 <li><a href="#tabs-1">Posts</a></li>7 <li><a href="#tabs-2">Comments</a></li>8 <li><a href="#tabs-3">Users</a></li>9 <li><a href="#tabs-4">Terms</a></li>10 </ul>4 <div id="imt-content-panel" class="imapie_settings_container"> 5 <!-- Native WordPress Tab Navigation --> 6 <nav class="nav-tab-wrapper" id="iwc-tab-nav" role="tablist"> 7 <a href="#iwc-tab-posts" class="nav-tab nav-tab-active" data-tab="posts" role="tab" aria-selected="true" aria-controls="iwc-tab-posts">Posts</a> 8 <a href="#iwc-tab-comments" class="nav-tab" data-tab="comments" role="tab" aria-selected="false" aria-controls="iwc-tab-comments">Comments</a> 9 <a href="#iwc-tab-users" class="nav-tab" data-tab="users" role="tab" aria-selected="false" aria-controls="iwc-tab-users">Users</a> 10 <a href="#iwc-tab-terms" class="nav-tab" data-tab="terms" role="tab" aria-selected="false" aria-controls="iwc-tab-terms">Terms</a> 11 </nav> 11 12 12 <div id="tabs-1"> 13 <form action="options.php" method="post" id="impaie_form_post"> 14 <?php 15 settings_fields('integromat_api_post'); 16 do_settings_sections('integromat_api_post'); 17 submit_button('Save Settings'); 18 ?> 19 </form> 20 </div> 13 <!-- Posts Tab --> 14 <div id="iwc-tab-posts" class="iwc-tab-content iwc-tab-active" role="tabpanel" aria-hidden="false" aria-labelledby="iwc-tab-posts"> 15 <form action="options.php" method="post" id="impaie_form_post"> 16 <?php 17 settings_fields('integromat_api_post'); 18 do_settings_sections('integromat_api_post'); 19 submit_button('Save Settings'); 20 ?> 21 </form> 22 </div> 21 23 22 <div id="tabs-2"> 23 <form action="options.php" method="post" id="impaie_form_comment"> 24 <?php 25 settings_fields('integromat_api_comment'); 26 do_settings_sections('integromat_api_comment'); 27 submit_button('Save Settings'); 28 ?> 29 </form> 30 </div> 24 <!-- Comments Tab --> 25 <div id="iwc-tab-comments" class="iwc-tab-content" role="tabpanel" aria-hidden="true" aria-labelledby="iwc-tab-comments"> 26 <form action="options.php" method="post" id="impaie_form_comment"> 27 <?php 28 settings_fields('integromat_api_comment'); 29 do_settings_sections('integromat_api_comment'); 30 submit_button('Save Settings'); 31 ?> 32 </form> 33 </div> 31 34 32 <div id="tabs-3"> 33 <form action="options.php" method="post" id="impaie_form_user"> 34 <?php 35 settings_fields('integromat_api_user'); 36 do_settings_sections('integromat_api_user'); 37 submit_button('Save Settings'); 38 ?> 39 </form> 40 </div> 35 <!-- Users Tab --> 36 <div id="iwc-tab-users" class="iwc-tab-content" role="tabpanel" aria-hidden="true" aria-labelledby="iwc-tab-users"> 37 <form action="options.php" method="post" id="impaie_form_user"> 38 <?php 39 settings_fields('integromat_api_user'); 40 do_settings_sections('integromat_api_user'); 41 submit_button('Save Settings'); 42 ?> 43 </form> 44 </div> 41 45 42 <div id="tabs-4">43 <form action="options.php" method="post" id="impaie_form_term">44 <input type="hidden" name="object_type" value="term">45 <?php46 settings_fields('integromat_api_term');47 do_settings_sections('integromat_api_term');48 submit_button('Save Settings');49 ?>50 </form>51 </ div>46 <!-- Terms Tab --> 47 <div id="iwc-tab-terms" class="iwc-tab-content" role="tabpanel" aria-hidden="true" aria-labelledby="iwc-tab-terms"> 48 <form action="options.php" method="post" id="impaie_form_term"> 49 <input type="hidden" name="object_type" value="term"> 50 <?php 51 settings_fields('integromat_api_term'); 52 do_settings_sections('integromat_api_term'); 53 submit_button('Save Settings'); 54 ?> 55 </form> 52 56 </div> 53 57 </div> -
integromat-connector/trunk/settings/template/custom_taxonomies.phtml
r2777334 r3361722 1 <?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?> 1 2 <h1><?php echo esc_html(get_admin_page_title()); ?></h1> 2 3 -
integromat-connector/trunk/settings/template/general_menu.phtml
r2777334 r3361722 1 <?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?> 1 2 <h1><?php echo esc_html(get_admin_page_title()); ?></h1> 2 3 <div id="imt-content-panel" class="imapie_settings_container">
Note: See TracChangeset
for help on using the changeset viewer.