<?php
/**
* Register blocks and there scripts
*/
add_action('init', function () {
/**
* Form Block
*/
// automatically load dependencies and version
$block_asset_file = include dirname(__DIR__) . '/build/form-block.asset.php';
$block = (array)json_decode(file_get_contents(__DIR__ . '/form/block.json'), true);
wp_register_script(
'ninja-forms/form',
plugins_url('../build/form-block.js', __FILE__),
$block_asset_file['dependencies'],
$block_asset_file['version']
);
register_block_type('ninja-forms/form', array_merge($block, [
'title' => esc_attr__('Ninja Form', 'ninja-forms'),
'render_callback' => function ($atts) {
$formID = isset($atts['formID']) ? $atts['formID'] : 1;
ob_start();
Ninja_Forms()->display( absint($formID), true );
return ob_get_clean();
},
'editor_script' => 'ninja-forms/form'
]));
/**
* Views Block
*/
// automatically load dependencies and version
$block_asset_file = include dirname(__DIR__) . '/build/sub-table-block.asset.php';
wp_register_script(
'ninja-forms/submissions-table/block',
plugins_url('../build/sub-table-block.js', __FILE__),
$block_asset_file['dependencies'],
$block_asset_file['version']
);
// Note: Token will be generated per-page in render_callback with specific form IDs
$render_asset_file = include dirname(__DIR__) . '/build/sub-table-render.asset.php';
wp_register_script(
'ninja-forms/submissions-table/render',
plugins_url('../build/sub-table-render.js', __FILE__),
$render_asset_file['dependencies'],
$render_asset_file['version']
);
register_block_type('ninja-forms/submissions-table', array(
'editor_script' => 'ninja-forms/submissions-table/block',
'render_callback' => function ($attributes, $content) {
if (isset($attributes['formID']) && $attributes['formID']) {
wp_enqueue_script('ninja-forms/submissions-table/render');
// Generate a token bound to THIS specific form ID only
$formId = absint($attributes['formID']);
$token = NinjaForms\Blocks\Authentication\TokenFactory::make();
$publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make();
// Create token with form ID binding and expiration
wp_localize_script('ninja-forms/submissions-table/render', 'ninjaFormsViews', [
'token' => $token->create($publicKey, array($formId)),
]);
// Enqueue signature fonts for proper display in Gutenberg block
wp_enqueue_style(
'nf-signature-fonts',
Ninja_Forms::$url . 'assets/fonts/signature/google-fonts.css',
[],
Ninja_Forms::VERSION
);
$className = 'ninja-forms-views-submissions-table';
if (isset($attributes['alignment'])) {
$className .= ' align' . $attributes['alignment'];
}
return sprintf("<div class='%s' data-attributes='%s'></div>", esc_attr($className),
esc_attr(wp_json_encode($attributes)));
}
}
));
/**
* Have Translations set in scripts via i18n package
* https://developer.wordpress.org/block-editor/packages/packages-i18n/
* https://developer.wordpress.org/reference/functions/wp_set_script_translations/
* https://developer.wordpress.org/block-editor/developers/internationalization/
*/
wp_set_script_translations( "ninja-forms/form", "ninja-forms", plugin_dir_path( __FILE__ ) . 'lang' );
wp_set_script_translations( "ninja-forms/submissions-table/block", "ninja-forms", plugin_dir_path( __FILE__ ) . 'lang' );
wp_set_script_translations( "ninja-forms/submissions-table/render", "ninja-forms", plugin_dir_path( __FILE__ ) . 'lang' );
});
/**
* Localize data for blocks
*/
add_action('admin_enqueue_scripts', function () {
//Conditionally load data for Blocks
$screen = get_current_screen();
if( is_null( $screen ) ) return;
if( ! $screen->is_block_editor() ) return;
//Get all forms, to base form selector on.
$formsBuilder = (new NinjaForms\Blocks\DataBuilder\FormsBuilderFactory)->make();
$forms = $formsBuilder->get();
if (!empty($forms)) {
//Escape for use in JavaScript
foreach ($forms as $key => $form) {
$forms[$key] = [
'formID' => absint($form['formID']),
'formTitle' => esc_textarea($form['formTitle'])
];
}
}
wp_localize_script('ninja-forms/form', 'nfFormsBlock', [
'forms' => $forms,//array keys escaped above
'homeUrl' => esc_url_raw( home_url() ), //URL to serve the iFrame that displays the form in blocks editor
'previewToken' => wp_create_nonce('nf_iframe' )
]);
// For block editor, provide a token that allows access to all forms
// SECURITY: Only users with appropriate capability can receive tokens for viewing submissions
// This prevents Contributors/Authors from accessing form submission data via the REST API
//
// Uses ninja_forms_admin_submissions_capabilities filter for consistency with Submissions menu
// Additional filter ninja_forms_views_token_capability allows specific customization for Views API
$views_capability = apply_filters(
'ninja_forms_views_token_capability',
apply_filters( 'ninja_forms_admin_submissions_capabilities', 'manage_options' )
);
if ( current_user_can( $views_capability ) ) {
$token = NinjaForms\Blocks\Authentication\TokenFactory::make();
$publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make();
$allFormIds = array_map(function($form) { return absint($form['formID']); }, $forms);
wp_localize_script('ninja-forms/submissions-table/block', 'ninjaFormsViews', [
'token' => $token->create($publicKey, $allFormIds),
]);
}
});
/**
* Register REST API routes related to blocks
*/
add_action('rest_api_init', function () {
/**
* Enhanced permission callback that validates token and checks form-level authorization.
*
* Security improvements:
* - Rate limiting to prevent DoS attacks
* - Validates token authenticity (hash, expiration)
* - Checks if token is authorized for the requested form ID
* - Falls back to WordPress capability check for admin users
*
* @param WP_REST_Request $request
* @return bool|WP_Error
*/
$tokenAuthenticationCallback = function (WP_REST_Request $request) {
// Check rate limit first (lightweight check)
$endpoint = $request->get_route();
$rateLimitCheck = NinjaForms\Blocks\Authentication\RateLimiter::check($endpoint);
if (is_wp_error($rateLimitCheck)) {
return $rateLimitCheck;
}
$tokenValidator = NinjaForms\Blocks\Authentication\TokenFactory::make();
$tokenHeader = $request->get_header('X-NinjaFormsViews-Auth');
$formId = $request->get_param('id');
// If user is logged in and has appropriate capability, allow access
// This provides fallback for admin users
// Uses same capability filter as token generation for consistency
$views_capability = apply_filters(
'ninja_forms_views_token_capability',
apply_filters( 'ninja_forms_admin_submissions_capabilities', 'manage_options' )
);
if (is_user_logged_in() && current_user_can($views_capability)) {
return true;
}
// Validate token with form ID authorization
if ($formId) {
return $tokenValidator->validate($tokenHeader, intval($formId));
}
// For routes without a specific form ID (like /forms list), only validate token structure
// The token must still be valid (not expired, proper signature)
return $tokenValidator->validate($tokenHeader);
};
register_rest_route('ninja-forms-views', 'forms', array(
'methods' => 'GET',
'callback' => function (WP_REST_Request $request) {
$tokenValidator = NinjaForms\Blocks\Authentication\TokenFactory::make();
$tokenHeader = $request->get_header('X-NinjaFormsViews-Auth');
// Get all forms
$formsBuilder = (new NinjaForms\Blocks\DataBuilder\FormsBuilderFactory)->make();
$allForms = $formsBuilder->get();
// If user has appropriate capability, return all forms
$views_capability = apply_filters(
'ninja_forms_views_token_capability',
apply_filters( 'ninja_forms_admin_submissions_capabilities', 'manage_options' )
);
if (is_user_logged_in() && current_user_can($views_capability)) {
return $allForms;
}
// Otherwise, filter forms based on token authorization
$authorizedFormIds = $tokenValidator->getFormIds($tokenHeader);
if ($authorizedFormIds === false) {
return new WP_Error('invalid_token', 'Invalid token', array('status' => 403));
}
// Filter to only return forms the token has access to
$filteredForms = array_filter($allForms, function($form) use ($authorizedFormIds) {
return in_array(intval($form['formID']), $authorizedFormIds, true);
});
return array_values($filteredForms);
},
'permission_callback' => $tokenAuthenticationCallback,
));
register_rest_route('ninja-forms-views', 'forms/(?P<id>\d+)/fields', [
'methods' => 'GET',
'args' => [
'id' => [
'required' => true,
'description' => esc_attr__('Unique identifier for the object.', 'ninja-forms'),
'type' => 'integer',
'validate_callback' => 'rest_validate_request_arg',
],
],
'callback' => function (WP_REST_Request $request) {
$fieldsBuilder = (new NinjaForms\Blocks\DataBuilder\FieldsBuilderFactory)->make(
$request->get_param('id')
);
return $fieldsBuilder->get();
},
'permission_callback' => $tokenAuthenticationCallback,
]);
register_rest_route('ninja-forms-views', 'forms/(?P<id>\d+)/submissions', [
'methods' => 'GET',
'args' => [
'id' => [
'required' => true,
'description' => esc_attr__('Unique identifier for the object.', 'ninja-forms'),
'type' => 'integer',
'validate_callback' => 'rest_validate_request_arg',
],
'perPage' => [
'description' => esc_attr__('Maximum number of items to be returned in result set.', 'ninja-forms'),
'type' => 'integer',
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
],
'page' => [
'description' => esc_attr__('Current page of the collection.', 'ninja-forms'),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
]
],
'callback' => function (WP_REST_Request $request) {
$submissionsBuilder = (new NinjaForms\Blocks\DataBuilder\SubmissionsBuilderFactory)->make(
$request->get_param('id'),
$request->get_param('perPage'),
$request->get_param('page')
);
return $submissionsBuilder->get();
},
'permission_callback' => $tokenAuthenticationCallback,
]);
/**
* Token Refresh Endpoint
*
* Generates a new token scoped to the same form ID as the previous token.
* Used for automatic token refresh when tokens expire or after secret rotation.
*
* SECURITY: Requires the old token to be provided. This ensures:
* - Only tokens that were legitimately issued can be refreshed
* - Tokens can only be refreshed for the same form ID
* - No reliance on spoofable Referer headers
*/
register_rest_route('ninja-forms-views', 'token/refresh', array(
'methods' => 'POST',
'callback' => function (WP_REST_Request $request) {
$tokenValidator = NinjaForms\Blocks\Authentication\TokenFactory::make();
// SECURITY: Require the old token for refresh
// This prevents attackers from generating tokens without having a legitimate one first
$oldToken = $request->get_header('X-NinjaFormsViews-Auth');
if (!$oldToken) {
return new WP_Error(
'missing_token',
__('A valid token is required for refresh. Include the current token in X-NinjaFormsViews-Auth header.', 'ninja-forms'),
array('status' => 401)
);
}
// Validate the old token's signature (allows expired tokens for refresh)
// This ensures the token was legitimately issued by this site
if (!$tokenValidator->validateSignatureOnly($oldToken)) {
return new WP_Error(
'invalid_token',
__('The provided token is invalid or has been tampered with.', 'ninja-forms'),
array('status' => 403)
);
}
// Extract form IDs from the old token - these are the only forms allowed for refresh
$authorizedFormIds = $tokenValidator->getFormIds($oldToken);
if ($authorizedFormIds === false || empty($authorizedFormIds)) {
return new WP_Error(
'invalid_token_payload',
__('Could not extract form authorization from token.', 'ninja-forms'),
array('status' => 403)
);
}
// Get the requested form ID (optional - defaults to first form in old token)
$formId = $request->get_param('formID');
// Check for legacy formIds parameter for backward compatibility
if (!$formId && $request->get_param('formIds')) {
$formIds = $request->get_param('formIds');
if (is_array($formIds) && !empty($formIds)) {
$formId = $formIds[0];
}
}
// If no form ID specified, use the first (and typically only) form from old token
if (!$formId) {
$formId = $authorizedFormIds[0];
}
// Sanitize form ID
$formId = absint($formId);
if (!$formId) {
return new WP_Error(
'invalid_form_id',
__('Valid form ID is required', 'ninja-forms'),
array('status' => 400)
);
}
// SECURITY: Verify the requested form ID was in the old token
// This prevents upgrading a single-form token to access other forms
if (!in_array($formId, array_map('intval', $authorizedFormIds), true)) {
return new WP_Error(
'unauthorized_form_access',
__('The requested form was not authorized in your original token.', 'ninja-forms'),
array('status' => 403)
);
}
// Validate that the form still exists
$form = Ninja_Forms()->form($formId)->get();
if (!$form) {
return new WP_Error(
'form_not_found',
__('The requested form does not exist', 'ninja-forms'),
array('status' => 404)
);
}
// Generate new token scoped to the single requested form
$publicKey = NinjaForms\Blocks\Authentication\KeyFactory::make(32);
$tokenGenerator = NinjaForms\Blocks\Authentication\TokenFactory::make();
$newToken = $tokenGenerator->create($publicKey, array($formId));
return array(
'token' => $newToken,
'publicKey' => $publicKey,
'expiresIn' => 900, // 15 minutes in seconds
'formID' => $formId,
);
},
'permission_callback' => function (WP_REST_Request $request) {
// Apply stricter rate limiting to refresh endpoint
$rateLimitCheck = NinjaForms\Blocks\Authentication\RateLimiter::check(
'/ninja-forms-views/token/refresh',
50, // limit: 50 requests
300 // window: 5 minutes
);
if (is_wp_error($rateLimitCheck)) {
return $rateLimitCheck; // Returns 429 Too Many Requests
}
return true; // Rate-limited, but token validation happens in callback
},
));
});
/**
* Handler for form preview iFrame used in Forms block
*/
add_action( 'wp_head', function () {
// check for preview and iframe get parameters
if( isset( $_GET[ 'nf_preview_form' ] ) && isset( $_GET[ 'nf_iframe' ] ) ){
if( ! wp_verify_nonce( $_GET['nf_iframe'], 'nf_iframe') ){
wp_die( esc_html__('Preview token failed validation', 'ninja-forms'));
exit;
}
//Attempt to get theme background color
$background = '#fff';
$supports = get_theme_support('editor-color-palette','background');
if( is_array($supports) ){
foreach($supports[0] as $index => $support ){
if( 'background' === $support['slug']){
$background = $support['color'];
break;
}
}
}
$js_lib_dir = Ninja_Forms::$url . 'assets/js/lib/';
$form_id = absint( $_GET[ 'nf_preview_form' ] );
// Style below: update width and height for particular form
?>
<style media="screen">
#wpadminbar {
display: none;
}
#nf-form-<?php echo $form_id; ?>-cont {
z-index: 90000001;
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
background-color: <?php echo sanitize_hex_color($background ); ?>;
}
div.site-branding, header.entry-header, .site-footer, header, .footer-nav-widgets-wrapper {
display:none !important;
}
</style>
<?php
// register our script to target the form iFrame in page builder
wp_register_script(
'ninja-forms-block-setup',
$js_lib_dir . 'blockFrameSetup.js',
array( 'underscore', 'jquery' )
);
wp_localize_script( 'ninja-forms-block-setup', 'ninjaFormsBlockSetup', array(
'form_id' => $form_id
) );
wp_enqueue_script( 'ninja-forms-block-setup' );
}
});
/**
* Schedule WP-Cron job for automatic secret rotation
*/
add_action('init', function() {
if (!wp_next_scheduled('ninja_forms_views_check_rotation')) {
wp_schedule_event(time(), 'daily', 'ninja_forms_views_check_rotation');
}
});
/**
* WP-Cron callback: Check if secret should be rotated and rotate if needed
*/
add_action('ninja_forms_views_check_rotation', function() {
if (NinjaForms\Blocks\Authentication\SecretStore::shouldRotate()) {
NinjaForms\Blocks\Authentication\SecretStore::rotate();
}
});
/**
* Clear scheduled events on plugin deactivation
*/
register_deactivation_hook(__FILE__, function() {
$timestamp = wp_next_scheduled('ninja_forms_views_check_rotation');
if ($timestamp) {
wp_unschedule_event($timestamp, 'ninja_forms_views_check_rotation');
}
});