Plugin Directory

Changeset 3361722


Ignore:
Timestamp:
09/15/2025 11:34:52 AM (7 months ago)
Author:
integromat
Message:

1.6.0

  • Security improvement: Granular API permissions
  • Security improvement: Configurable rate limiting
  • New feature: Enhanced file upload validation
  • New feature: Request payload size limits
  • New feature: API key rotation
  • New feature: Purge log
  • Fix multiple vulnerabilities
Location:
integromat-connector
Files:
44 added
24 edited

Legend:

Unmodified
Added
Removed
  • integromat-connector/trunk/api/authentication.php

    r2784613 r3361722  
    2626
    2727        if ( $skip ) {
    28             $log && \Integromat\Logger::write( implode( ', ', $codes ) );
     28            $log && \Integromat\Logger::write( implode( ';', $codes ) );
    2929            return $result;
    3030        }
     
    3232        if ( isset( $_SERVER['HTTP_IWC_API_KEY'] ) && ! empty( $_SERVER['HTTP_IWC_API_KEY'] ) ) {
    3333
    34             $token = sanitize_text_field( $_SERVER['HTTP_IWC_API_KEY'] );
     34            $token = sanitize_text_field( wp_unslash( $_SERVER['HTTP_IWC_API_KEY'] ) );
    3535
    3636            if ( strlen( $token ) !== \Integromat\Api_Token::API_TOKEN_LENGTH || ! \Integromat\Api_Token::is_valid( $token ) ) {
     
    3838                \Integromat\Rest_Response::render_error( 401, 'Provided API key is invalid', 'invalid_token' );
    3939            } 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                }
    4177                $log && \Integromat\Logger::write( 7 );
    4278                \Integromat\Rest_Request::dispatch();
  • integromat-connector/trunk/assets/iwc.css

    r2529734 r3361722  
    1616    background-color: white;
    1717    padding: 30px;
     18    margin-right: 20px;
    1819}
    1920#imt-content-panel section {
     
    4243
    4344.imapie_settings_container.wait {
    44     filter: opacity(20%)
     45    filter: opacity(20%);
    4546}
    4647
    4748.imapie_settings_container.wait * {
    4849    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 */
     279input[type="checkbox"]:focus,
     280input[type="text"]:focus,
     281select:focus,
     282textarea: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;
    49645}
    50646
     
    52648    cursor: pointer;
    53649    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 containers
    107 ----------------------------------*/
    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 helpers
    134 ----------------------------------*/
    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;
    162650}
    163651
     
    172660}
    173661
    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  
    33    $(document).ready(function () {
    44
     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">&times;</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">&times;</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
    5391        $('#iwc-api-key-value').on('click', function() {
    6392            $(this).select();
    7393        });
    8394
    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           
    14481            $('.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                });
    23498
    24499            return false;
    25500        });
    26501
     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
    27549        $('.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');
    32559            } 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           
    43577            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            }
    44593        });
    45594    });
  • integromat-connector/trunk/class/class-api-token.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Api_Token {
     
    2628    public static function initiate() {
    2729        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 );
    2933        }
    3034    }
     
    3640     */
    3741    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 );
    3945    }
    4046
    41 
    4247    /**
    43      * Generate random string
     48     * Regenerate API token
    4449     *
    45      * @param int    $length
    46      * @param string $charlist
    47      * @return string
     50     * @return string New token
    4851     * @throws \Exception
    4952     */
    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;
    6757    }
    6858
  • integromat-connector/trunk/class/class-guard.php

    r2793954 r3361722  
    11<?php
    22namespace Integromat;
     3
     4defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    35
    46class Guard {
     
    810     * @return bool
    911     */
    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 );
    1636    }
    1737}
  • integromat-connector/trunk/class/class-logger.php

    r2784613 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Logger {
    68    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     */
    919    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 ) );
    1158    }
    1259
     
    1663        } else {
    1764            $fsize = filesize( self::get_file_location() );
    18             if ( $fsize > ( self::MAXFILESIZEMB * 1000000 ) ) {
     65            if ( $fsize > ( self::MAXFILESIZEMB * self::BYTES_IN_MB ) ) {
    1966                self::create_file();
    2067            }
     
    2370
    2471    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( '' ) );
    9074        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;
    93120    }
    94121
     
    97124    }
    98125
     126    /**
     127     * Encrypt data using secure AES-256-CBC
     128     *
     129     * @param string $data
     130     * @return string
     131     */
    99132    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     */
    103148    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';
    105167    }
    106168
     
    111173
    112174    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       
    113186        $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(),
    117193        );
    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";
    121195    }
    122196
     
    130204    public static function get_plain_file_content() {
    131205        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.' ) ) );
    133207        }
    134208        $enc_data = file_get_contents( self::get_file_location() );
     
    136210    }
    137211
    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 
    146212    public static function download() {
    147213        $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       
    148218        header( 'Content-Type: application/octet-stream' );
    149219        header( 'Content-Transfer-Encoding: Binary' );
    150220        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;
    152224        exit;
    153225    }
     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    }
    154250}
  • integromat-connector/trunk/class/class-rest-request.php

    r2915666 r3361722  
    33namespace Integromat;
    44
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
     6
    57class Rest_Request {
    68
    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
    2392            if ( ! empty( $_FILES['file'] ) ) {
    2493                self::upload_media();
     94                return; // upload_media handles its own response
    2595            }
    2696        }
     
    30100        $response_data = $server->response_to_data( $response, false );
    31101
    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       
    39110        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;
    40136    }
    41137
     
    46142     */
    47143    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       
    49150        if ( ! empty( $response->headers ) ) {
    50151            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' );
    55161        if ( is_object( $response_data ) && is_object( $response_data->data ) && (int) $response_data->data->status > 0 ) {
    56162            http_response_code( $response_data->data->status );
    57163        }
    58         die( $response_json );
     164       
     165        // Respond with JSON-encoded data and exit
     166        echo wp_json_encode( $response_data );
     167        exit();
    59168    }
    60169
    61170
    62171    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 );
    86306        } 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' ),
    103320        );
    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       
    132330        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    }
    168338
    169339    /**
     
    180350        }
    181351    }
    182 
    183352}
  • integromat-connector/trunk/class/class-rest-response.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Rest_Response {
     
    6264     * @param string $error_message
    6365     * @param string $error_code
     66     * @param array  $headers Optional headers to include
    6467     */
    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() ) {
    7369        http_response_code( $status_code );
    7470        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;
    7788    }
    7889}
  • integromat-connector/trunk/class/class-user.php

    r2883308 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class User {
     
    1113     */
    1214    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       
    1417        if ( empty( $users ) ) {
    1518            return 0;
     
    1821        // Prioritize user ID 1 (default admin).
    1922        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 ) ) {
    2124                return $user->data->ID;
    22             };
     25            }
    2326        }
    2427
    2528        // Search for another admin, if user ID 1 doesn't exist or hasn't administrator role.
    2629        foreach ( $users as $user ) {
    27             if ( in_array( 'administrator', $user->roles ) ) {
     30            if ( in_array( 'administrator', $user->roles, true ) ) {
    2831                return $user->data->ID;
    29             };
     32            }
    3033        }
    3134
     
    3538
    3639    /**
    37      * Log user in
     40     * Set current user context for API requests with specific permissions
    3841     *
    3942     * @param int $user_id
     43     * @param string $endpoint The REST endpoint being accessed
     44     * @param string $method HTTP method
     45     * @return bool Success status
    4046     */
    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
    4355        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;
    4470    }
    4571
  • integromat-connector/trunk/index.php

    r3257687 r3361722  
    33/**
    44 * @package Integromat_Connector
    5  * @version 1.5.10
     5 * @version 1.6.0
    66 */
    77
     
    1111Author: Celonis s.r.o.
    1212Author URI: https://www.make.com/en?utm_source=wordpress&utm_medium=partner&utm_campaign=wordpress-partner-make
    13 Version: 1.5.10
     13Version: 1.6.0
     14License: GPL v2 or later
     15License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1416*/
    1517
     
    1719define('IWC_PLUGIN_NAME_SAFE', 'integromat-wordpress-connector');
    1820define('IWC_MENUITEM_IDENTIFIER', 'integromat_custom_fields');
     21define('IWC_PLUGIN_VERSION', '1.6.0');
    1922
    2023require __DIR__ . '/class/class-user.php';
     
    2427require __DIR__ . '/class/class-guard.php';
    2528require __DIR__ . '/class/class-logger.php';
     29require __DIR__ . '/class/class-api-permissions.php';
     30require __DIR__ . '/class/class-rate-limiter.php';
     31require __DIR__ . '/class/class-file-validator.php';
    2632
    2733require __DIR__ . '/api/authentication.php';
     
    3440$controller = new \Integromat\Controller();
    3541$controller->init();
     42
     43// Initialize API permissions
     44\Integromat\Api_Permissions::init();
    3645
    3746// Custom CSS, JS.
     
    4655        wp_enqueue_style(
    4756            'integromat_css',
    48             plugin_dir_url(__FILE__) . 'assets/iwc.css'
     57            plugin_dir_url(__FILE__) . 'assets/iwc.css',
     58            [],
     59            IWC_PLUGIN_VERSION
    4960        );
    5061        wp_enqueue_script(
    5162            'integromat_js',
    5263            plugin_dir_url(__FILE__) . 'assets/iwc.js',
    53             ['jquery-ui-tabs']
     64            ['jquery'],
     65            IWC_PLUGIN_VERSION,
     66            true
    5467        );
     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        ));
    5576    }
    5677);
     78
     79// AJAX handler for API key regeneration
     80add_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
     116add_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
     153add_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
     181register_activation_hook( __FILE__, function() {
     182    \Integromat\Api_Permissions::add_api_capabilities();
     183    iwc_set_default_settings();
     184});
     185
     186register_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 */
     194function 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 */
     205function 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  
    33Tags: make, integromat, rest, api, rest api
    44Requires at least: 5.0
    5 Tested up to:  6.7.2
     5Tested up to: 6.8
    66Requires PHP: 7.2
    7 Stable tag: 1.5.10
     7Stable tag: 1.6.0
    88License: GPLv2 or later
    99
     
    4545
    4646== 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
    4756= 1.5.10 =
    4857* Fix a bug introduced in previous fix regarding PHP 7 compatibility
  • integromat-connector/trunk/settings/class-controller.php

    r2883308 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Controller {
     
    1214            function () {
    1315                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 ) ) {
    1518                    // Posts.
    1619                    require_once __DIR__ . '/object-types/class-post-meta.php';
     
    3437                }
    3538
    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' ) ) {
    3741                    // Taxonomies.
    3842                    require_once __DIR__ . '/object-types/custom-taxonomy.php';
     
    4145                require_once __DIR__ . '/object-types/general.php';
    4246                add_general_menu();
     47               
     48                // Security settings
     49                require_once __DIR__ . '/object-types/security.php';
     50                add_security_menu();
    4351            }
    4452        );
  • integromat-connector/trunk/settings/class-meta-object.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Meta_Object {
     
    1315    public function get_meta_items( $table ) {
    1416        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();
    2136    }
    2237
  • integromat-connector/trunk/settings/events.php

    r2785241 r3361722  
    66        // Download a log file.
    77        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' ) ) {
    99                \Integromat\Logger::download();
    1010            } else {
    11                 die( __( 'Wrong nonce', 'textdomain' ) );
     11                wp_die( esc_html__( 'Security check failed', 'integromat-connector' ), esc_html__( 'Error', 'integromat-connector' ), array( 'response' => 403 ) );
    1212            }
    1313        }
  • integromat-connector/trunk/settings/object-types/class-comments-meta.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Comments_Meta extends Meta_Object {
     
    1214        global $wpdb;
    1315        $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        ));
    1519
    1620        add_settings_section(
    1721            'integromat_api_section_comments',
    18             __('', 'integromat_api_comment'),
     22            __('Comments Metadata Settings', 'integromat-connector'),
    1923            function ()  {
    2024                ?>
    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>
    2226                    <p><a class="uncheck_all" data-status="0">Un/check all</a></p>
    2327                <?php
     
    3034                IWC_FIELD_PREFIX . $meta_item,
    3135
    32                 __($meta_item, 'integromat_api_comment'),
     36                esc_html($meta_item),
    3337                function ($args) use($meta_item) {
    3438                    $options = get_option('integromat_api_options_comment');
     
    5155    }
    5256
     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
    5376}
    54 
  • integromat-connector/trunk/settings/object-types/class-post-meta.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Posts_Meta extends Meta_Object {
     
    1214    public function init() {
    1315        $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        ) );
    1519
    1620        add_settings_section(
    1721            '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.
    1923            function () {
    2024                ?>
    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>
    2226                    <p><a class="uncheck_all" data-status="0">Un/check all</a></p>
    2327                <?php
     
    7175            add_settings_field(
    7276                IWC_FIELD_PREFIX . $meta_item,
    73                 __( $meta_item, 'integromat_api_post' ),
     77                esc_html( $meta_item ),
    7478                function ( $args ) use ( $meta_item, $object_type, $last_object_type ) {
    7579                    $options = get_option( 'integromat_api_options_post' );
     
    116120                add_settings_field(
    117121                    IWC_FIELD_PREFIX . $meta_item,
    118                     __( $meta_item, 'integromat_api_post' ),
     122                    esc_html( $meta_item ),
    119123                    function ( $args ) use ( $meta_item ) {
    120124                        $options    = get_option( 'integromat_api_options_post' );
     
    146150    public function get_post_meta_items() {
    147151        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";
    155157        $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();
    157161    }
     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
    158182}
  • integromat-connector/trunk/settings/object-types/class-term-meta.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Terms_Meta extends Meta_Object {
     
    810        global $wpdb;
    911        $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        ) );
    1115
    1216        add_settings_section(
    1317            'integromat_api_section_terms',
    14             __( '', 'integromat_api_term' ),
     18            __( 'Terms Metadata Settings', 'integromat-connector' ),
    1519            function () {
    1620                ?>
    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>
    1822                <p><a class="uncheck_all" data-status="0">Un/check all</a></p>
    1923                <?php
     
    2529            add_settings_field(
    2630                IWC_FIELD_PREFIX . $meta_item,
    27                 __( $meta_item, 'integromat_api_term' ),
     31                esc_html( $meta_item ),
    2832                function ( $args ) use ( $meta_item ) {
    2933                    $options = get_option( 'integromat_api_options_term' );
     
    4549        }
    4650    }
     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    }
    4770}
  • integromat-connector/trunk/settings/object-types/class-user-meta.php

    r2783423 r3361722  
    22
    33namespace Integromat;
     4
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
    46
    57class Users_Meta extends Meta_Object {
     
    1214        global $wpdb;
    1315        $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        ) );
    1519
    1620        add_settings_section(
    1721            'integromat_api_section_users',
    18             __( '', 'integromat_api_user' ),
     22            __( 'Users Metadata Settings', 'integromat-connector' ),
    1923            function () {
    2024                ?>
    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>
    2226                <p><a class="uncheck_all" data-status="0">Un/check all</a></p>
    2327                <?php
     
    2933            add_settings_field(
    3034                IWC_FIELD_PREFIX . $meta_item,
    31                 __( $meta_item, 'integromat_api_user' ),
     35                esc_html( $meta_item ),
    3236                function ( $args ) use ( $meta_item ) {
    3337                    $options = get_option( 'integromat_api_options_user' );
     
    5054    }
    5155
     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
    5275}
    5376
  • integromat-connector/trunk/settings/object-types/custom-taxonomy.php

    r2883308 r3361722  
    33namespace Integromat;
    44
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
     6
    57function 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    ) );
    711
    812    add_settings_section(
     
    1115        function () {
    1216            ?>
    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>
    1418                <p><a class="uncheck_all" data-status="0">Un/check all</a></p>
    1519            <?php
     
    4246    }
    4347}
     48
     49/**
     50 * Sanitize taxonomy options
     51 *
     52 * @param array $input
     53 * @return array
     54 */
     55function 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  
    11<?php
     2
    23namespace Integromat;
    34
     5defined( 'ABSPATH' ) || die( 'No direct access allowed' );
     6
    47function 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    }
    633
    734    add_settings_section(
     
    1845        function ( $args ) {
    1946            $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>
    2669                <p class="comment">Use this token when creating a new connection in the WordPress app.</p>
    2770            <?php
     
    3275            'api_key' => \Integromat\Api_Token::get(),
    3376        )
     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()
    34175    );
    35176
     
    63204            $href    = $args['enabled'] ? "href={$url}&_wpnonce={$nonce}" : '';
    64205            ?>
    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>
    66216                <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 the file) 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.
    69219                </p>
    70220            <?php
  • integromat-connector/trunk/settings/render.php

    r2911623 r3361722  
    55    function () {
    66        add_menu_page(
    7             'Make',
     7            'General Settings',
    88            'Make',
    99            'manage_options',
     
    1818            },
    1919            '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            }
    2137        );
    2238
     
    5167            }
    5268        );
     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        );
    5385    }
    5486);
  • integromat-connector/trunk/settings/template/customFields.phtml

    r2529734 r3361722  
     1<?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?>
    12<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
    23
    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>
    1112
    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>
    2123
    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>
    3134
    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>
    4145
    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                 <?php
    46                 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>
    5256    </div>
    5357</div>
  • integromat-connector/trunk/settings/template/custom_taxonomies.phtml

    r2777334 r3361722  
     1<?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?>
    12<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
    23
  • integromat-connector/trunk/settings/template/general_menu.phtml

    r2777334 r3361722  
     1<?php defined( 'ABSPATH' ) || die( 'No direct access allowed' ); ?>
    12<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
    23<div id="imt-content-panel" class="imapie_settings_container">
Note: See TracChangeset for help on using the changeset viewer.