Submission.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <?php if ( ! defined( 'ABSPATH' ) ) exit;
  2. class NF_AJAX_Controllers_Submission extends NF_Abstracts_Controller
  3. {
  4. protected $_form_data = array();
  5. protected $_form_cache = array();
  6. protected $_preview_data = array();
  7. protected $_form_id = '';
  8. public function __construct()
  9. {
  10. if( isset( $_POST[ 'nf_resume' ] ) && isset( $_COOKIE[ 'nf_wp_session' ] ) ){
  11. add_action( 'ninja_forms_loaded', array( $this, 'resume' ) );
  12. return;
  13. }
  14. if( isset( $_POST['formData'] ) ) {
  15. $this->_form_data = json_decode( $_POST['formData'], TRUE );
  16. // php5.2 fallback
  17. if( ! $this->_form_data ) $this->_form_data = json_decode( stripslashes( $_POST['formData'] ), TRUE );
  18. }
  19. add_action( 'wp_ajax_nf_ajax_submit', array( $this, 'submit' ) );
  20. add_action( 'wp_ajax_nopriv_nf_ajax_submit', array( $this, 'submit' ) );
  21. add_action( 'wp_ajax_nf_ajax_resume', array( $this, 'resume' ) );
  22. add_action( 'wp_ajax_nopriv_nf_ajax_resume', array( $this, 'resume' ) );
  23. }
  24. public function submit()
  25. {
  26. $nonce_name = 'ninja_forms_display_nonce';
  27. /**
  28. * We've got to get the 'nonce_ts' to append to the nonce name to get
  29. * the unique nonce we created
  30. * */
  31. if( isset( $_REQUEST[ 'nonce_ts' ] ) && 0 < strlen( $_REQUEST[ 'nonce_ts' ] ) ) {
  32. $nonce_name = $nonce_name . "_" . $_REQUEST[ 'nonce_ts' ];
  33. }
  34. check_ajax_referer( $nonce_name, 'security' );
  35. register_shutdown_function( array( $this, 'shutdown' ) );
  36. $this->form_data_check();
  37. $this->_form_id = $this->_form_data['id'];
  38. // If we don't have a numeric form ID...
  39. if ( ! is_numeric( $this->_form_id ) ) {
  40. // Kick the request out without processing.
  41. $this->_errors[] = __( 'Form does not exist.', 'ninja-forms' );
  42. $this->_respond();
  43. }
  44. if( $this->is_preview() ) {
  45. $this->_form_cache = get_user_option( 'nf_form_preview_' . $this->_form_id );
  46. if( ! $this->_form_cache ){
  47. $this->_errors[ 'preview' ] = __( 'Preview does not exist.', 'ninja-forms' );
  48. $this->_respond();
  49. }
  50. } else {
  51. $this->_form_cache = WPN_Helper::get_nf_cache( $this->_form_id );
  52. }
  53. // TODO: Update Conditional Logic to preserve field ID => [ Settings, ID ] structure.
  54. $this->_form_data = apply_filters( 'ninja_forms_submit_data', $this->_form_data );
  55. $this->process();
  56. }
  57. public function resume()
  58. {
  59. $this->_form_data = Ninja_Forms()->session()->get( 'nf_processing_form_data' );
  60. $this->_form_cache = Ninja_Forms()->session()->get( 'nf_processing_form_cache' );
  61. $this->_data = Ninja_Forms()->session()->get( 'nf_processing_data' );
  62. $this->_data[ 'resume' ] = $_POST[ 'nf_resume' ];
  63. $this->_form_id = $this->_data[ 'form_id' ];
  64. unset( $this->_data[ 'halt' ] );
  65. unset( $this->_data[ 'actions' ][ 'redirect' ] );
  66. $this->process();
  67. }
  68. protected function process()
  69. {
  70. // Init Field Merge Tags.
  71. $field_merge_tags = Ninja_Forms()->merge_tags[ 'fields' ];
  72. $field_merge_tags->set_form_id( $this->_form_id );
  73. // Init Calc Merge Tags.
  74. $calcs_merge_tags = Ninja_Forms()->merge_tags[ 'calcs' ];
  75. $form_settings = $this->_form_cache[ 'settings' ];
  76. if( ! $form_settings ){
  77. $form = Ninja_Forms()->form( $this->_form_id )->get();
  78. $form_settings = $form->get_settings();
  79. }
  80. $this->_data[ 'form_id' ] = $this->_form_data[ 'form_id' ] = $this->_form_id;
  81. $this->_data[ 'settings' ] = $form_settings;
  82. $this->_data[ 'settings' ][ 'is_preview' ] = $this->is_preview();
  83. $this->_data[ 'extra' ] = $this->_form_data[ 'extra' ];
  84. /*
  85. |--------------------------------------------------------------------------
  86. | Fields
  87. |--------------------------------------------------------------------------
  88. */
  89. if( $this->is_preview() ){
  90. $form_fields = $this->_form_cache[ 'fields' ];
  91. } else {
  92. $form_fields = Ninja_Forms()->form($this->_form_id)->get_fields();
  93. }
  94. /**
  95. * The Field Processing Loop.
  96. *
  97. * There can only be one!
  98. * For performance reasons, this should be the only time that the fields array is traversed.
  99. * Anything needing to loop through fields should integrate here.
  100. */
  101. $validate_fields = apply_filters( 'ninja_forms_validate_fields', true, $this->_data );
  102. foreach( $form_fields as $key => $field ){
  103. if( is_object( $field ) ) {
  104. $field = array(
  105. 'id' => $field->get_id(),
  106. 'settings' => $field->get_settings()
  107. );
  108. }
  109. /** Get the field ID */
  110. /*
  111. * TODO: Refactor data structures to match.
  112. * Preview: Field IDs are stored as the associated array key.
  113. * Publish: Field IDs are stored as an array key=>value pair.
  114. */
  115. if( $this->is_preview() ){
  116. $field[ 'id' ] = $key;
  117. }
  118. // Duplicate field ID as single variable for more readable array access.
  119. $field_id = $field[ 'id' ];
  120. // Check that the field ID exists in the submitted for data and has a submitted value.
  121. if( isset( $this->_form_data[ 'fields' ][ $field_id ] ) && isset( $this->_form_data[ 'fields' ][ $field_id ][ 'value' ] ) ){
  122. $field[ 'value' ] = $this->_form_data[ 'fields' ][ $field_id ][ 'value' ];
  123. } else {
  124. $field[ 'value' ] = '';
  125. }
  126. // Duplicate field value to settings and top level array item for backwards compatible access (ie Save Action).
  127. $field[ 'settings' ][ 'value' ] = $field[ 'value' ];
  128. // Duplicate field value to form cache for passing to the action filter.
  129. $this->_form_cache[ 'fields' ][ $key ][ 'settings' ][ 'value' ] = $this->_form_data[ 'fields' ][ $field_id ][ 'value' ];
  130. // Duplicate the Field ID for access as a setting.
  131. $field[ 'settings' ][ 'id' ] = $field[ 'id' ];
  132. // Combine with submitted data.
  133. $field = array_merge( $field, $this->_form_data[ 'fields' ][ $field_id ] );
  134. // Flatten the field array.
  135. $field = array_merge( $field, $field[ 'settings' ] );
  136. /** Validate the Field */
  137. if( $validate_fields && ! isset( $this->_data[ 'resume' ] ) ){
  138. $this->validate_field( $field );
  139. }
  140. /** Process the Field */
  141. if( ! isset( $this->_data[ 'resume' ] ) ) {
  142. $this->process_field($field);
  143. }
  144. $field = array_merge( $field, $this->_form_data[ 'fields' ][ $field_id ] );
  145. // Check for field errors after processing.
  146. if ( isset( $this->_form_data['errors']['fields'][ $field_id ] ) ) {
  147. $this->_errors['fields'][ $field_id ] = $this->_form_data['errors']['fields'][ $field_id ];
  148. $this->_respond();
  149. }
  150. /** Populate Field Merge Tag */
  151. $field_merge_tags->add_field( $field );
  152. $this->_data[ 'fields' ][ $field_id ] = $field;
  153. $this->_data[ 'fields_by_key' ][ $field[ 'key' ] ] = $field;
  154. }
  155. /*
  156. |--------------------------------------------------------------------------
  157. | Check for unique field settings.
  158. |--------------------------------------------------------------------------
  159. */
  160. if ( isset ( $this->_data[ 'settings' ][ 'unique_field' ] ) && ! empty( $this->_data[ 'settings' ][ 'unique_field' ] ) ) {
  161. /*
  162. * Get our unique field
  163. */
  164. $unique_field_key = $this->_data[ 'settings' ][ 'unique_field' ];
  165. $unique_field_error = $this->_data[ 'settings' ][ 'unique_field_error' ];
  166. $unique_field_id = $this->_data[ 'fields_by_key' ][ $unique_field_key ][ 'id' ];
  167. $unique_field_value = $this->_data[ 'fields_by_key' ][ $unique_field_key ][ 'value' ];
  168. if ( is_array( $unique_field_value ) ) {
  169. $unique_field_value = serialize( $unique_field_value );
  170. }
  171. /*
  172. * Check our db for the value submitted.
  173. */
  174. global $wpdb;
  175. $sql = $wpdb->prepare( "SELECT COUNT(m.meta_id) FROM `" . $wpdb->prefix . "postmeta` AS m LEFT JOIN `" . $wpdb->prefix . "posts` AS p ON p.id = m.post_id WHERE m.meta_key = '_field_%d' AND m.meta_value = '%s' AND p.post_status = 'publish'", $unique_field_id, $unique_field_value );
  176. $result = $wpdb->get_results( $sql, 'ARRAY_N' );
  177. if ( intval( $result[ 0 ][ 0 ] ) > 0 ) {
  178. $this->_errors['fields'][ $unique_field_id ] = array( 'slug' => 'unique_field', 'message' => $unique_field_error );
  179. $this->_respond();
  180. }
  181. }
  182. /*
  183. |--------------------------------------------------------------------------
  184. | Calculations
  185. |--------------------------------------------------------------------------
  186. */
  187. if( isset( $this->_form_cache[ 'settings' ][ 'calculations' ] ) ) {
  188. /**
  189. * The Calculation Processing Loop
  190. */
  191. foreach( $this->_form_cache[ 'settings' ][ 'calculations' ] as $calc ){
  192. $eq = apply_filters( 'ninja_forms_calc_setting', $calc[ 'eq' ] );
  193. // Scrub unmerged tags (ie deleted/non-existent fields/calcs, etc).
  194. $eq = preg_replace( '/{([a-zA-Z0-9]|:|_|-)*}/', 0, $eq);
  195. /**
  196. * PHP doesn't evaluate empty strings to numbers. So check
  197. * for any string for the decimal place
  198. **/
  199. $dec = ( isset( $calc[ 'dec' ] ) && '' != $calc[ 'dec' ] ) ?
  200. $calc[ 'dec' ] : 2;
  201. $calcs_merge_tags->set_merge_tags( $calc[ 'name' ], $eq, $dec, $this->_form_data['settings']['decimal_point'], $this->_form_data['settings']['thousands_sep'] );
  202. $this->_data[ 'extra' ][ 'calculations' ][ $calc[ 'name' ] ] = array(
  203. 'raw' => $calc[ 'eq' ],
  204. 'parsed' => $eq,
  205. 'value' => $calcs_merge_tags->get_formatted_calc_value( $calc[ 'name' ], $dec, $this->_form_data['settings']['decimal_point'], $this->_form_data['settings']['thousands_sep'] ),
  206. );
  207. }
  208. }
  209. /*
  210. |--------------------------------------------------------------------------
  211. | Actions
  212. |--------------------------------------------------------------------------
  213. */
  214. /*
  215. * TODO: This section has become convoluted, but will be refactored along with the submission controller.
  216. */
  217. if( isset( $this->_data[ 'resume' ] ) && $this->_data[ 'resume' ] ){
  218. // On Resume Submission, the action data is loaded form the session.
  219. // This section intentionally left blank.
  220. } elseif( ! $this->is_preview() ) {
  221. // Published forms rely on the Database for the "truth" about Actions.
  222. $actions = Ninja_Forms()->form($this->_form_id)->get_actions();
  223. $this->_form_cache[ 'actions' ] = array();
  224. foreach( $actions as $action ){
  225. $action_id = $action->get_id();
  226. $this->_form_cache[ 'actions' ][ $action_id ] = array(
  227. 'id' => $action_id,
  228. 'settings' => $action->get_settings()
  229. );
  230. }
  231. } else {
  232. // Previews uses user option for stored data.
  233. $preview_data = get_user_option( 'nf_form_preview_' . $this->_form_id );
  234. $this->_form_cache[ 'actions' ] = $preview_data[ 'actions' ];
  235. }
  236. /* END form cache bypass. */
  237. // Sort Actions by Timing Order, then by Priority Order.
  238. usort( $this->_form_cache[ 'actions' ], array( $this, 'sort_form_actions' ) );
  239. /*
  240. * Filter Actions so that they can be pragmatically disabled by add-ons.
  241. *
  242. * ninja_forms_submission_actions
  243. * ninja_forms_submission_actions_preview
  244. */
  245. $this->_form_cache[ 'actions' ] = apply_filters( 'ninja_forms_submission_actions', $this->_form_cache[ 'actions' ], $this->_form_cache, $this->_form_data );
  246. if( $this->is_preview() ) {
  247. $this->_form_cache['actions'] = apply_filters('ninja_forms_submission_actions_preview', $this->_form_cache['actions'], $this->_form_cache);
  248. }
  249. // Initialize the process actions log.
  250. if( ! isset( $this->_data[ 'processed_actions' ] ) ) $this->_data[ 'processed_actions' ] = array();
  251. /*
  252. * Merging extra data that may have been added by fields during processing so that the values aren't lost when we enter the action loop.
  253. */
  254. $this->_data[ 'extra' ] = array_merge( $this->_data[ 'extra' ], $this->_form_data[ 'extra' ] );
  255. /**
  256. * The Action Processing Loop
  257. */
  258. foreach( $this->_form_cache[ 'actions' ] as $key => $action ){
  259. /** Get the action ID */
  260. /*
  261. * TODO: Refactor data structures to match.
  262. * Preview: Action IDs are stored as the associated array key.
  263. * Publish: Action IDs are stored as an array key=>value pair.
  264. */
  265. if( $this->is_preview() ){
  266. $action[ 'id' ] = $key;
  267. }
  268. // Duplicate the Action ID for access as a setting.
  269. $action[ 'settings' ][ 'id' ] = $action[ 'id' ];
  270. // Duplicate action ID as single variable for more readable array access.
  271. $action_id = $action[ 'id' ];
  272. // If an action has already run (ie resume submission), do not re-process.
  273. if( in_array( $action[ 'id' ], $this->_data[ 'processed_actions' ] ) ) continue;
  274. $action[ 'settings' ] = apply_filters( 'ninja_forms_run_action_settings', $action[ 'settings' ], $this->_form_id, $action[ 'id' ], $this->_form_data['settings'] );
  275. if( $this->is_preview() ){
  276. $action[ 'settings' ] = apply_filters( 'ninja_forms_run_action_settings_preview', $action[ 'settings' ], $this->_form_id, $action[ 'id' ], $this->_form_data['settings'] );
  277. }
  278. if( ! $action[ 'settings' ][ 'active' ] ) continue;
  279. if( ! apply_filters( 'ninja_forms_run_action_type_' . $action[ 'settings' ][ 'type' ], TRUE ) ) continue;
  280. $type = $action[ 'settings' ][ 'type' ];
  281. if( ! is_string( $type ) ) continue;
  282. /*
  283. * test if Ninja_Forms()->actions[ $type ] is not empty
  284. */
  285. if(isset(Ninja_Forms()->actions[ $type ]))
  286. {
  287. $action_class = Ninja_Forms()->actions[ $type ];
  288. if( ! method_exists( $action_class, 'process' ) ) continue;
  289. if( $data = $action_class->process($action[ 'settings' ], $this->_form_id, $this->_data ) )
  290. {
  291. $this->_data = apply_filters( 'ninja_forms_post_run_action_type_' . $action[ 'settings' ][ 'type' ], $data );
  292. }
  293. }
  294. // $this->_data[ 'actions' ][ $type ][] = $action;
  295. $this->maybe_halt( $action[ 'id' ] );
  296. }
  297. do_action( 'ninja_forms_after_submission', $this->_data );
  298. $this->_respond();
  299. }
  300. protected function validate_field( $field_settings )
  301. {
  302. $field_settings = apply_filters( 'ninja_forms_pre_validate_field_settings', $field_settings );
  303. if( ! is_string( $field_settings['type'] ) ) return;
  304. $field_class = Ninja_Forms()->fields[ $field_settings['type'] ];
  305. if( ! method_exists( $field_class, 'validate' ) ) return;
  306. if( $errors = $field_class->validate( $field_settings, $this->_form_data ) ){
  307. $field_id = $field_settings[ 'id' ];
  308. $this->_errors[ 'fields' ][ $field_id ] = $errors;
  309. $this->_respond();
  310. }
  311. }
  312. protected function process_field( $field_settings )
  313. {
  314. if( ! is_string( $field_settings['type'] ) ) return;
  315. $field_class = Ninja_Forms()->fields[ $field_settings['type'] ];
  316. if( ! method_exists( $field_class, 'process' ) ) return;
  317. if( $data = $field_class->process( $field_settings, $this->_form_data ) ){
  318. $this->_form_data = $data;
  319. }
  320. }
  321. protected function maybe_halt( $action_id )
  322. {
  323. if( isset( $this->_data[ 'errors' ] ) && $this->_data[ 'errors' ] ){
  324. $this->_respond();
  325. }
  326. if( isset( $this->_data[ 'halt' ] ) && $this->_data[ 'halt' ] ){
  327. Ninja_Forms()->session()->set( 'nf_processing_data', $this->_data );
  328. Ninja_Forms()->session()->set( 'nf_processing_form_data', $this->_form_data );
  329. Ninja_Forms()->session()->set( 'nf_processing_form_cache', $this->_form_cache );
  330. $this->_respond();
  331. }
  332. array_push( $this->_data[ 'processed_actions' ], $action_id );
  333. }
  334. protected function sort_form_actions( $a, $b )
  335. {
  336. if( is_object( $a ) ) {
  337. if( ! isset( Ninja_Forms()->actions[ $a->get_setting( 'type' ) ] ) ) return -1;
  338. $a = Ninja_Forms()->actions[ $a->get_setting( 'type' ) ];
  339. } else {
  340. if( ! isset( Ninja_Forms()->actions[ $a[ 'settings' ][ 'type' ] ] ) ) return -1;
  341. $a = Ninja_Forms()->actions[ $a[ 'settings' ][ 'type' ] ];
  342. }
  343. if( is_object( $b ) ) {
  344. if( ! isset( Ninja_Forms()->actions[ $b->get_setting( 'type' ) ] ) ) return 1;
  345. $b = Ninja_Forms()->actions[ $b->get_setting( 'type' ) ];
  346. } else {
  347. if( ! isset( Ninja_Forms()->actions[ $b[ 'settings' ][ 'type' ] ] ) ) return 1;
  348. $b = Ninja_Forms()->actions[ $b[ 'settings' ][ 'type' ] ];
  349. }
  350. if ( $a->get_timing() == $b->get_timing() ) {
  351. if ( $a->get_priority() == $b->get_priority() ) {
  352. return 0;
  353. }
  354. return ( $a->get_priority() < $b->get_priority() ) ? -1 : 1;
  355. }
  356. return ( $a->get_timing() < $b->get_timing() ) ? -1 : 1;
  357. }
  358. public function shutdown()
  359. {
  360. $error = error_get_last();
  361. if( $error !== NULL && in_array( $error[ 'type' ], array( E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ) ) ) {
  362. $this->_errors[ 'form' ][ 'last' ] = __( 'The server encountered an error during processing.', 'ninja-forms' );
  363. if( current_user_can( 'manage_options' ) && isset( $error[ 'message' ] ) ){
  364. $this->_errors[ 'form' ][ 'last_admin' ] = '<pre>' . $error[ 'message' ] . '</pre>';
  365. }
  366. $this->_errors[ 'last' ] = $error;
  367. Ninja_Forms()->logger()->emergency( $error[ 'message' ] );
  368. $this->_respond();
  369. }
  370. }
  371. protected function form_data_check()
  372. {
  373. if( $this->_form_data ) return;
  374. if( function_exists( 'json_last_error' ) // Function not supported in php5.2
  375. && function_exists( 'json_last_error_msg' )// Function not supported in php5.4
  376. && json_last_error() ){
  377. $this->_errors[] = json_last_error_msg();
  378. } else {
  379. $this->_errors[] = __( 'An unexpected error occurred.', 'ninja-forms' );
  380. }
  381. $this->_respond();
  382. }
  383. protected function is_preview()
  384. {
  385. if( ! isset( $this->_form_data[ 'settings' ][ 'is_preview' ] ) ) return false;
  386. return $this->_form_data[ 'settings' ][ 'is_preview' ];
  387. }
  388. /*
  389. * Overwrite method for parent class.
  390. */
  391. protected function _respond( $data = array() )
  392. {
  393. // Set a content type of JSON for the purpose of previnting XSS attacks.
  394. header( 'Content-Type: application/json' );
  395. // Call the parent method.
  396. parent::_respond();
  397. }
  398. }