<?php

namespace FormTools;

require_once(realpath(__DIR__ . "/../library.php"));

use PDO, Exception;


class API
{
    private static $version = "2.0.0";
    private static $releaseDate = "20180113";
    private static $systemErrors = array(304);

    /**
     * API constructor. Single param with the following:
     *      "init_core" => true (default). This prevents the constructor doing anything.
     *      "start_sessions" => false (default)
     * @param array $settings
     */
    public function __construct ($settings = array()) {
        $init_core = isset($settings["init_core"]) ? $settings["init_core"] : true;
        $start_sessions = isset($settings["start_sessions"]) ? $settings["start_sessions"] : true;

        if ($init_core) {
            Core::init(array(
                "start_sessions" => $start_sessions
            ));
        }
    }

    public static function getVersion() {
        return self::$version;
    }

    public static function getReleaseDate() {
        return self::$releaseDate;
    }

    /**
     * This function lets you display the content generated from an export type into a webpage. You can
     * use this function on any export type in the database - even the ones that have been marked as "hidden". Note:
     * this function requires the Export Manager to be installed and enabled.
     *
     * @param integer $form_id
     * @param integer $view_id
     * @param integer $export_type_id
     * @param integer $page_num (defaults to 1)
     * @param array $options optional parameter that lets you configure the appearance of the data in a variety of ways.
     *
     *         num_per_page              - (integer) by default, it returns the number of results per page specified by
     *                                     the View. This setting overrides that.
     *         submission_ids            - (integer or array of integers) this limits the results returned to the submission ID
     *                                     or submission IDs specified in this field
     *         order                     - (string) the database column name with a -DESC or -ASC suffix (e.g. col_1-ASC).
     *         page_num_identifier       - (string) passed via the query string to denote what page it's on (default: "page")
     *         show_columns_only         - (boolean) limits the fields that are displayed to those fields marked as "Column"
     *                                     in the View. Defaults to false.
     *         return_as_string          - (boolean) if this value is set to true, instead of outputting the result it returns
     *                                     the HTML as a string.
     *         pagination_theme          - (string) the pagination links (<< 1 2 3 ...) HTML is generated by the pagination.tpl
     *                                     template, found in each of the theme folders. Generally this file is the same for
     *                                     all themes, but in case it isn't, this setting lets you choose the theme folder with
     *                                     which to render the HTML.
     *         pagination_location       - (string) accepts the values "top" (the default), "bottom", "both" or "none". This
     *                                     determines where (if anywhere) the pagination links should appear. By default it only
     *                                     appears at the top of the page, but you can set this value to either "both" or "bottom"
     *                                     to have it appear there instead / as well.
     *
     * @return mixed the return value of this function depends on the API settings & the options passed to it. Namely:
     *
     *     If error:
     *        if $api_debug == true, the error page will be displayed displaying the error code.
     *        if $api_debug == false, it returns an array with two indexes:
     *                   [0] false
     *                   [1] the API error code
     *     If successful:
     *        if "return_as_string" option key is set, it returns an array with two indexes:
     *                   [0] true
     *                   [1] the HTML content
     *        if "return_as_string" not set, it just prints the HTML to the page (the default behaviour)
     */
    public function showSubmissions($form_id, $view_id, $export_type_id, $page_num = 1, $options = array())
    {
        $db = Core::$db;
        $root_dir = Core::getRootDir();
        $smarty = Templates::getPageRenderSmarty("default");

        if (!Modules::checkModuleEnabled("export_manager")) {
            return self::processError(400);
        }

        // check the form ID, View ID and export ID are valid
        $db->query("SELECT count(*) FROM {PREFIX}forms WHERE form_id = :form_id");
        $db->bind("form_id", $form_id);
        $db->execute();

        if ($db->fetch(PDO::FETCH_COLUMN) === 0) {
            return self::processError(401);
        }

        $db->query("
            SELECT count(*)
            FROM {PREFIX}views
            WHERE form_id = :form_id AND
                  view_id = :view_id
        ");
        $db->bindAll(array(
            "form_id" => $form_id,
            "view_id" => $view_id
        ));
        $db->execute();
        if ($db->fetch(PDO::FETCH_COLUMN) == 0) {
            return self::processError(402);
        }

        $db->query("SELECT count(*) FROM {PREFIX}module_export_types WHERE export_type_id = :export_type_id");
        $db->bind("export_type_id", $export_type_id);
        $db->execute();

        if ($db->fetch(PDO::FETCH_COLUMN) == 0) {
            return self::processError(403);
        }

        // okay, now lets figure out what needs to be displayed & rendered
        $export_manager = Modules::instantiateModule("export_manager");
        $smarty->addPluginsDir(array("$root_dir/modules/export_manager/smarty_plugins"));

        $form_info = Forms::getForm($form_id);
        $form_fields = Fields::getFormFields($form_id, array("include_field_type_info" => true, "include_field_settings" => true));
        $view_info = Views::getView($view_id);
        $export_type_info = $export_manager->getExportType($export_type_id);
        $export_group_id = $export_type_info["export_group_id"];
        $export_group_info = $export_manager->getExportGroup($export_group_id);

        // number of submissions per page (an integer or "all")
        $num_per_page = $view_info["num_submissions_per_page"];
        if (isset($options["num_per_page"])) {
            $num_per_page = $options["num_per_page"];
        }

        $order = "{$view_info["default_sort_field"]}-{$view_info["default_sort_field_order"]}";
        if (isset($options["order"])) {
            $order = $options["order"];
        }

        $display_fields = array();
        $columns = "all";
        if (isset($options["show_columns_only"]) && $options["show_columns_only"]) {
            $columns = array();

            foreach ($view_info["columns"] as $view_field_info) {
                $curr_field_id = $view_field_info["field_id"];
                foreach ($form_fields as $form_field_info) {
                    if ($form_field_info["field_id"] != $curr_field_id) {
                        continue;
                    }

                    $display_fields[] = array_merge($form_field_info, $view_field_info);
                    $columns[] = $form_field_info["col_name"];
                }
            }
        } else {
            foreach ($view_info["fields"] as $view_field_info) {
                $curr_field_id = $view_field_info["field_id"];
                foreach ($form_fields as $form_field_info) {
                    if ($form_field_info["field_id"] != $curr_field_id) {
                        continue;
                    }

                    $display_fields[] = array_merge($form_field_info, $view_field_info);
                }
            }
        }

        $submission_ids = array();
        if (isset($options["submission_ids"])) {
            if (is_numeric($options["submission_ids"])) {
                $submission_ids[] = $options["submission_ids"];
            } else {
                if (is_array($submission_ids)) {
                    $submission_ids = $options["submission_ids"];
                }
            }
        }

        // perform the almighty search query
        $results_info = Submissions::searchSubmissions($form_id, $view_id, $num_per_page, $page_num, $order, $columns, array(), $submission_ids);
        $search_num_results = $results_info["search_num_results"];

        $settings = Settings::get();

        // now build the list of information we're going to send to the export type smarty template
        $placeholders = $export_manager->getExportFilenamePlaceholderHash();
        $placeholders["export_group_id"] = $export_group_id;
        $placeholders["export_type_id"] = $export_type_id;
        $placeholders["export_group_results"] = "all";
        $placeholders["same_page"] = General::getCleanPhpSelf();
        $placeholders["display_fields"] = $display_fields;
        $placeholders["submissions"] = $results_info["search_rows"];
        $placeholders["num_results"] = $results_info["search_num_results"];
        $placeholders["view_num_results"] = $results_info["view_num_results"];
        $placeholders["form_info"] = $form_info;
        $placeholders["view_info"] = $view_info;
        $placeholders["field_types"] = FieldTypes::get(true);
        $placeholders["settings"] = $settings;
        $placeholders["date_format"] = $settings["default_date_format"];
        $placeholders["timezone_offset"] = $settings["timezone_offset"];

        // pull out a few things into top level placeholders for easy use
        $placeholders["form_name"] = $form_info["form_name"];
        $placeholders["form_id"] = $form_id;
        $placeholders["form_url"] = $form_info["form_url"];
        $placeholders["view_name"] = $view_info["view_name"];
        $placeholders["view_id"] = $view_id;

        $placeholders["export_group_name"] = General::createSlug(General::evalSmartyString($export_group_info["group_name"]));
        $placeholders["export_group_type"] = General::createSlug(General::evalSmartyString($export_type_info["export_type_name"]));
        $placeholders["filename"] = General::evalSmartyString($export_type_info["filename"], $placeholders, $smarty->getPluginsDir());

        $template = $export_type_info["export_type_smarty_template"];
        $placeholders["export_type_name"] = $export_type_info["export_type_name"];
        $export_type_smarty_template = General::evalSmartyString($template, $placeholders, $smarty->getPluginsDir());

        // if we're not displaying all results on the single page, generate the pagination HTML
        $pagination = "";
        if ($num_per_page != "all") {
            $page_num_identifier = isset($options["page_num_identifier"]) ? $options["page_num_identifier"] : "page";
            $theme = isset($options["pagination_theme"]) ? $options["pagination_theme"] : $settings["default_theme"];
            $pagination = General::getPageNav($search_num_results, $num_per_page, $page_num, "", $page_num_identifier,
            $theme);
        }

        $pagination_location = (isset($options["pagination_location"])) ? $options["pagination_location"] : "top";

        switch ($pagination_location) {
            case "top":
                $html = $pagination . $export_type_smarty_template;
                break;
            case "both":
                $html = $pagination . $export_type_smarty_template . $pagination;
                break;
            case "bottom":
                $html = $export_type_smarty_template . $pagination;
                break;
            case "none":
                $html = $export_type_smarty_template;
                break;

            // in case the user entered an invalid value
            default:
                return self::processError(404);
                break;
        }

        if (isset($options["return_as_string"]) && $options["return_as_string"]) {
            return array(true, $html);
        } else {
            echo $html;
        }
    }


    /**
     * Displays an individual form submission. Note: this function is NOT compatible with tabs - it won't render the
     * information with whatever tabs were defined.
     *
     * @param integer $form_id
     * @param integer $submission_id
     * @param integer $view_id
     * @param integer $submission_id
     */
    public function showSubmission($form_id, $view_id, $export_type_id, $submission_id)
    {
        $options = array(
            "submission_ids" => $submission_id,
            "num_per_page" => "all" // prevents the pagination from appearing
        );
        $this->showSubmissions($form_id, $view_id, $export_type_id, "", $options);
    }


    /**
     * Displays the total number of submissions in a form or form View.
     *
     * @param integer $form_id
     * @param integer $view_id
     */
    public function showSubmissionCount($form_id, $view_id = "")
    {
        return Submissions::getSubmissionCount($form_id, $view_id);
    }


    /**
     * Creates a new, blank, unfinalized form submission and returns the new submission ID.
     *
     * @param integer $form_id
     * @param boolean $finalized whether or not the submission should be finalized. False by default.
     * @param array $default_values a hash of Database Columns => Values, letting you init the submission with those
     *    values entered.
     * @return mixed $submission_id returns the new submission ID if successful. If there's an error, it either
     *   redirects to an error page or returns the error info. This depends on your $g_api_debug setting in your
     *   config.php file.
     */
    public function createSubmission($form_id, $finalized = false, $values = array())
    {
        $db = Core::$db;

        if (!Forms::checkFormExists($form_id)) {
            return self::processError(500);
        }

        $now = General::getCurrentDatetime();

        $values["is_finalized"] = ($finalized) ? "yes" : "no";
        $values["ip_address"] = $_SERVER["REMOTE_ADDR"];
        $values["submission_date"] = $now;
        $values["last_modified_date"] = $now;
        list ($cols_str, $placeholders_str) = $db->getInsertStatementParams($values);

        try {
            $db->query("
                INSERT INTO {PREFIX}form_{$form_id} ($cols_str)
                VALUES ($placeholders_str)
            ");
            $db->bindAll($values);
            $db->execute();
        } catch (Exception $e) {
            return self::processError(501, array("debugging" => $e->getMessage()));
        }

        return $db->getInsertId();
    }


    /**
     * This function was written to *significantly* simplify the job of integrating code-submission forms with Form
     * Tools. This, when used in conjunction with ft_api_form_process_page(), effectively does away with the need
     * to embed any special PHP logic in your forms to ensure the data gets submitted to Form Tools properly.
     *
     * The function does the following:
     *
     *    - starts sessions (used to store the form data as the user progresses through the form)
     *    - get / returns the unique submission ID. It creates a unique submission ID record in the database for
     *         this submission; but it only shows up in the Form Tools UI if you explicitly tell Form Tools that the
     *         submission is complete
     *    - returns all values already submitted in the form, to let you pre-populate the fields if you want
     *
     * @param integer $form_id this field is only required for the FIRST page in your form.
     * @param string $namespace - a hash key to defined where in sessions the form information should be stored. Most
     *      users probably won't care about this; it's for programmers who want a little more control over the content
     *      of sessions (it's stored in: $_SESSION["form_tools_form"] by default). Note: if you choose to define your
     *      own namespace, make sure you pass in the "namespace" setting in the final $settings parameter for the
     *      ft_api_form_process_page() function - otherwise it won't know what submission or form to process!
     * @return array [0] the submission ID
     *               [1] a hash of form values
     */
    public function initFormPage($form_id = "", $mode = "live", $namespace = "form_tools_form")
    {
        Core::startSessions("api_form");

        if (!isset($_SESSION[$namespace]) || empty($_SESSION[$namespace])) {
            $_SESSION[$namespace] = array();

            // here, form_id should have been set: this is the FIRST page of (potentially) a multi-page form
            switch ($mode) {
                case "test":
                    $_SESSION[$namespace]["form_tools_form_id"] = "test";
                    $_SESSION[$namespace]["form_tools_submission_id"] = "test";
                    break;

                case "initialize":
                    // if form ID is blank here, chances are a user just put through their test submission an has returned
                    // to a multi-page form page. In this situation, the sessions have been emptied, but this function is
                    // called PRIOR to ft_api_process_form, which does the job of auto-redirecting to whatever page is
                    // specified by the user
                    if (empty($form_id)) {
                        return $_SESSION[$namespace];
                    }
                    $_SESSION[$namespace]["form_tools_form_id"] = $form_id;
                    $_SESSION[$namespace]["form_tools_submission_id"] = "initializing";
                    $_SESSION[$namespace]["form_tools_initialize_form"] = 1;
                    break;

                case "live":
                    // if form ID is blank here, chances are a user is just returning to a multi-page form page
                    // after putting through the submission. In this situation, the sessions have been emptied, but
                    // this function is called PRIOR to ft_api_process_form, which does the job of auto-redirecting
                    // to whatever page is specified by the user
                    if (empty($form_id)) {
                        return $_SESSION[$namespace];
                    }

                    $submission_id = self::createSubmission($form_id);
                    $_SESSION[$namespace]["form_tools_form_id"] = $form_id;
                    $_SESSION[$namespace]["form_tools_submission_id"] = $submission_id;
                    break;

                default:
                    return self::processError(200);
                    break;
            }
        }

        return $_SESSION[$namespace];
    }


    /**
     * Clears sessions after succesfully completing a form.
     *
     * @param string $namespace (optional);
     */
    public function clearFormSessions($namespace = "form_tools_form")
    {
        $_SESSION[$namespace] = "";
        unset($_SESSION[$namespace]);
    }


    /**
     * Processes a form submission, either for a single page of a multi-page form or the entire form itself. If the
     * "submit_button_name key exists in $params (i.e. if the user just submitted the form), it updates the database for
     * the submission ID.
     *
     * Assumption: the initFormPage() function has been called on the page prior to calling this function.
     *
     * @param array $params
     *
     *     Required keys:
     *        "submit_button": the "name" attribute value of the form submit button
     *        "form_data": the contents of $_POST (or $_GET, if "method" setting is set to "GET" ... )
     *        "file_data": the contents of $_FILES (only needed if your form contained file fields)
     *
     *     Optional keys:
     *        "next_page": the URL (relative or absolute) of which page to redirect to (e.g. the next page
     *               in the form or the "thankyou" page).
     *        "finalize": this tells the function to finalize the submission. This prevents it being subsequently
     *               editable via this function and makes the submission appear in the Form Tools UI.
     *        "no_sessions_url": for multi-page forms it's a good idea to pass along this value. It should be the URL
     *               of a page (usually the FIRST page in the form sequence) where the user will be redirected to if
     *               they didn't start the form from the first page. It ensures the form submission gets created &
     *               submitted properly.
     *        "may_update_finalized_submissions": true / false (true by default)
     *        "namespace": if you specified a custom namespace for ft_api_init_form_page, for where the form values will
     *               be stored temporarily in sessions, you need to pass that same value to this function - otherwise
     *               it won't be able to retrieve the form and submission ID
     *        "send_emails": (boolean). By default, Form Tools will trigger any emails that have been attached to the
     *               "on submission" event ONLY when the submission is finalized (finalize=true). This setting provides
     *               you with direct control over when the emails get sent. If not specified, will use the default
     *               behaviour.
     *        "has_captcha": (boolean). This needs to be set to true if the POST contains a captcha.
     *
     * @return mixed ordinarily, this function will just redirect the user to whatever URL is specified in the
     *        "next_page" key. But if that value isn't set, it returns an array:
     *               [0] success / false
     *               [1] if failure, the API Error Code, otherwise blank
     */
    public function processFormSubmission($params)
    {
        $multi_val_delimiter = Core::getMultiFieldValDelimiter();
        $LANG = Core::$L;
        $db = Core::$db;

        // the form data parameter must ALWAYS be defined
        if (!isset($params["form_data"])) {
            return self::processError(306);
        }

        // special case: if "form_tools_delete_image_field__[fieldname]" exists, the user is just deleting an image
        // already uploaded through the form using the HTML generated by the ft_api_display_image_field function.
        // In this case, we process the page normally - even though the form data wasn't submitted & the page may
        // contain nothing in $form_data
        $is_deleting_file = false;
        $namespace = isset($params["namespace"]) ? $params["namespace"] : "form_tools_form";
        $form_id = isset($_SESSION[$namespace]["form_tools_form_id"]) ? $_SESSION[$namespace]["form_tools_form_id"] : "";
        $submission_id = isset($_SESSION[$namespace]["form_tools_submission_id"]) ? $_SESSION[$namespace]["form_tools_submission_id"] : "";

        while (list($key, $value) = each($params["form_data"])) {
            if (preg_match("/form_tools_delete_image_field__(.*)$/", $key, $matches)) {
                $file_field_to_delete = $matches[1];
                $is_deleting_file = true;
                $field_col = Fields::getFieldColByFieldName($form_id, $file_field_to_delete);

                try {
                    $db->query("
                        UPDATE {PREFIX}form_{$form_id}
                        SET $field_col = :val
                        WHERE submission_id = :submission_id
                    ");
                    $db->bindAll(array(
                        "val" => "",
                        "submission_id" => $submission_id
                    ));
                    $db->execute();

                    $path = $_SESSION[$namespace][$file_field_to_delete]["file_upload_dir"];
                    $filename = $_SESSION[$namespace][$file_field_to_delete]["filename"];
                    @unlink("$path/$filename");

                    unset($_SESSION[$namespace][$file_field_to_delete]);
                    unset($params["form_data"][$key]);
                } catch (Exception $e) {

                }
            }
        }

        // check the submission exists
        if (is_numeric($form_id) && is_numeric($submission_id) && !Submissions::checkSubmissionExists($form_id, $submission_id)) {
            return self::processError(305, array("debugging" => "{$LANG["phrase_submission_id"]}: $submission_id"));
        }

        // extract the submission ID and form ID from sessions
        $form_data = $params["form_data"];
        $form_id = isset($_SESSION[$namespace]["form_tools_form_id"]) ? $_SESSION[$namespace]["form_tools_form_id"] : "";
        $submission_id = isset($_SESSION[$namespace]["form_tools_submission_id"]) ? $_SESSION[$namespace]["form_tools_submission_id"] : "";
        $has_captcha = isset($params["has_captcha"]) ? $params["has_captcha"] : false;
        $no_sessions_url = isset($params["no_sessions_url"]) ? $params["no_sessions_url"] : false;

        if (!isset($_GET["ft_sessions_url_override"]) && (empty($form_id) || empty($submission_id))) {
            if (!empty($no_sessions_url)) {
                header("location: $no_sessions_url");
                exit;
            } else {
                return self::processError(300);
            }
        }

        // if the user is neither deleting a file or making a regular form submission, it means they've just
        // arrived at the page. Cool! Do nothing!
        if (!$is_deleting_file && !isset($params["form_data"][$params["submit_button"]])) {
            return;
        }

        $next_page = isset($params["next_page"]) ? $params["next_page"] : "";
        $finalize = isset($params["finalize"]) ? $params["finalize"] : false;
        $namespace = isset($params["namespace"]) ? $params["namespace"] : "form_tools_form";
        $may_update_finalized_submissions = isset($params["may_update_finalized_submissions"]) ? $params["may_update_finalized_submissions"] : true;

        // if we're in test mode, we don't do anything with the database - just store the fields in
        // sessions to emulate
        if ($form_id == "test" || $submission_id == "test") {
            reset($form_data);
            while (list($field_name, $value) = each($form_data)) {
                $_SESSION[$namespace][$field_name] = $value;
            }
        } else {
            if (isset($_SESSION[$namespace]["form_tools_initialize_form"])) {
                // only process the form if this submission is being set to be finalized
                if ($finalize) {
                    // if the user is just putting through a test submission and we've reached the finalization step,
                    // overwrite $form_data with ALL the
                    $all_form_data = array_merge($_SESSION[$namespace], $form_data);
                    Forms::initializeForm($all_form_data);
                }

                reset($form_data);
                while (list($field_name, $value) = each($form_data)) {
                    $_SESSION[$namespace][$field_name] = $value;
                }
            }

            // otherwise it's a standard form submission for a fully set up form, with - ostensibly - a valid
            // submission ID and form ID. Update the submission for whatever info is in $form_data
            else {
                // check the form ID is valid
                if (!Forms::checkFormExists($form_id)) {
                    return self::processError(301);
                }

                // check the submission ID isn't finalized
                if (!$may_update_finalized_submissions && Submissions::checkSubmissionFinalized($form_id, $submission_id)) {
                    return self::processError(302, array("debugging" => "{$LANG["phrase_submission_id"]}: $submission_id"));
                }

                $form_info = Forms::getForm($form_id);

                // check to see if this form has been disabled
                if ($form_info["is_active"] == "no") {
                    if (isset($form_data["form_tools_inactive_form_redirect_url"])) {
                        header("location: {$form_data["form_tools_inactive_form_redirect_url"]}");
                        exit;
                    }
                    return self::processError(303);
                }

                extract(Hooks::processHookCalls("start", compact("form_info", "form_id", "form_data"), array("form_data")), EXTR_OVERWRITE);

                // get a list of the custom form fields (i.e. non-system) for this form
                $form_fields = Fields::getFormFields($form_id, array("include_field_type_info" => true));

                $custom_form_fields = array();
                $file_fields = array();
                foreach ($form_fields as $field_info) {
                    $field_id = $field_info["field_id"];
                    $is_system_field = $field_info["is_system_field"];
                    $field_name = $field_info["field_name"];

                    // ignore system fields
                    if ($is_system_field == "yes") {
                        continue;
                    }

                    if ($field_info["is_file_field"] == "no") {
                        $custom_form_fields[$field_name] = array(
                            "field_id" => $field_id,
                            "col_name" => $field_info["col_name"],
                            "field_title" => $field_info["field_title"],
                            "include_on_redirect" => $field_info["include_on_redirect"],
                            "field_type_id" => $field_info["field_type_id"],
                            "is_date_field" => $field_info["is_date_field"]
                        );
                    } else {
                        $file_fields[] = array(
                            "field_id" => $field_id,
                            "field_info" => $field_info
                        );
                    }
                }

                // now examine the contents of the POST/GET submission and get a list of those fields
                // which we're going to update
                $valid_form_fields = array();
                while (list($form_field, $value) = each($form_data)) {
                    if (array_key_exists($form_field, $custom_form_fields)) {
                        $curr_form_field = $custom_form_fields[$form_field];
                        $cleaned_value = $value;
                        if (is_array($value)) {
                            if ($form_info["submission_strip_tags"] == "yes") {
                                for ($i = 0; $i < count($value); $i++) {
                                    $value[$i] = strip_tags($value[$i]);
                                }
                            }

                            $cleaned_value = implode("$multi_val_delimiter", $value);
                        } else {
                            if ($form_info["submission_strip_tags"] == "yes") {
                                $cleaned_value = strip_tags($value);
                            }
                        }

                        $valid_form_fields[$curr_form_field["col_name"]] = $cleaned_value;
                    }
                }

                $now = General::getCurrentDatetime();
                $is_finalized = ($finalize) ? "yes" : "no";

                $valid_form_fields["last_modified_date"] = $now;
                $valid_form_fields["ip_address"] = $_SERVER["REMOTE_ADDR"];
                $set_statements = $db->getUpdateStatements($valid_form_fields);

                // update the database submission info & upload files. Note: we don't do ANYTHING if the
                // form_tools_ignore_submission key is set in the POST data
                if (!isset($form_data["form_tools_ignore_submission"])) {

                    // construct our query. Note that we do TWO queries: one if there was no CAPTCHA sent with this
                    // post (which automatically finalizes the result), and one if there WAS. For the latter, the submission
                    // is finalized later
                    if ($has_captcha && $finalize) {
                        $db->query("
                            UPDATE {PREFIX}form_$form_id
                            SET    $set_statements
                            WHERE  submission_id = :submission_id
                        ");
                        $db->bindAll($valid_form_fields);
                        $db->bind("submission_id", $submission_id);
                    } else {
                        // only update the is_finalized setting if $may_update_finalized_submissions === false
                        if (!$finalize && $may_update_finalized_submissions) {
                            $is_finalized_clause = "";
                        } else {
                            $is_finalized_clause = ", is_finalized = :is_finalized";
                        }

                        $db->query("
                            UPDATE {PREFIX}form_$form_id
                            SET    $set_statements
                                   $is_finalized_clause
                            WHERE  submission_id = :submission_id
                        ");
                        $db->bindAll($valid_form_fields);
                        $db->bind("submission_id", $submission_id);

                        if (!empty($is_finalized_clause)) {
                            $db->bind("is_finalized", $is_finalized);
                        }
                    }

                    // only process the query if the form_tools_ignore_submission key isn't defined
                    try {
                        $db->execute();
                    } catch (Exception $e) {
                        return self::processError(304, array(
                            "debugging" => "Failed query in <b>" . __FUNCTION__ . ", " . __FILE__ . "</b>, line " . __LINE__ . ": " . $e->getMessage()
                        ));
                    }

                    // used for uploading files. The error handling is incomplete here, like previous versions. Although the hooks
                    // are permitted to return values, they're not used
                    extract(Hooks::processHookCalls("manage_files", compact("form_id", "submission_id", "file_fields", "namespace"), array("success", "message")), EXTR_OVERWRITE);
                }

                // store all the info in sessions
                reset($form_data);
                while (list($field_name, $value) = each($form_data)) {
                    $_SESSION[$namespace][$field_name] = $value;
                }
            }
        }

        // was there a reCAPTCHA response? If so, a recaptcha was just submitted, check it was entered correctly
        $passes_captcha = true;
        if ($has_captcha) {
            $passes_captcha = false;
            $resp = $this->validateRecaptcha($params["form_data"]["g-recaptcha-response"]);

            if ($resp->isSuccess()) {
                $passes_captcha = true;

                // if the developer wanted the submission to be finalized at this step, do so - it wasn't earlier!
                if ($finalize) {
                    $db->query("
                        UPDATE {PREFIX}form_$form_id
                        SET    is_finalized = 'yes'
                        WHERE  submission_id = :submission_id
                    ");
                    $db->bind("submission_id", $submission_id);
                    $db->execute();
                }
            } else {
                // register the recaptcha response as a global, which can be picked up silently by ft_api_display_captcha to
                // let them know they entered it wrong
                $GLOBALS["g_api_recaptcha_error"] = $resp->getErrorCodes();
            }
        }

        if ($passes_captcha && !empty($next_page) && !$is_deleting_file) {
            // if the user wasn't putting through a test submission or initializing the form, we can send safely
            // send emails at this juncture, but ONLY if it was just finalized OR if the send_emails parameter
            // allows for it
            if ($form_id != "test" && $submission_id != "test" && !isset($_SESSION[$namespace]["form_tools_initialize_form"])
                && !isset($form_data["form_tools_ignore_submission"])) {
                // send any emails attached to the on_submission trigger
                if (isset($params["send_emails"]) && $params["send_emails"] === true) {
                    Emails::sendEmails("on_submission", $form_id, $submission_id);
                } else {
                    if (isset($is_finalized) && $is_finalized == "yes" && (!isset($params["send_emails"]) || $params["send_emails"] !== false)) {
                        Emails::sendEmails("on_submission", $form_id, $submission_id);
                    }
                }
            }

            header("location: $next_page");
            exit;
        }

        return array(true, "");
    }


    /**
     * This function saves you the effort of writing the PHP needed to display an image that's been uploaded through
     * your form. The function is used in forms that include file upload fields - and specifically, file upload fields
     * where users are uploading *images only*. It displays any images already uploaded through the form field,
     * and (by default) a "delete" button to let them delete the image.
     *
     *
     * @param hash $params a hash with the following keys:
     *   Required keys
     *     "field_name" - the name of the file input field
     *
     *   Optional keys
     *     "width"  - adds a "width" attribute to the image, with this value
     *     "height" - adds a "width" attribute to the image, with this value
     *     "namespace"  - only required if you specified a custom namespace in the original initFormPage method.
     *     "hide_delete_button" - by default, whenever the field already has an image uploaded, this function will
     *          display the image as well as a "Delete" button. If this value is passed & set to true, the delete
     *          button will be removed (handy for "Review" pages).
     *     "delete_button_label" - by default, "Delete file"
     */
    public function displayImageField($params)
    {
        if (empty($params["field_name"])) {
            return;
        }

        $field_name = $params["field_name"];
        $namespace = isset($params["namespace"]) ? $params["namespace"] : "form_tools_form";

        // if an image hasn't already been uploaded through this field, do nothing
        if (!isset($_SESSION[$namespace][$field_name]) || !is_array($_SESSION[$namespace][$field_name]) ||
            !isset($_SESSION[$namespace][$field_name]["filename"])) {
            return;
        }

        $file_upload_url = $_SESSION[$namespace][$field_name]["file_upload_url"];
        $filename = $_SESSION[$namespace][$field_name]["filename"];
        $width = isset($params["width"]) ? "width=\"{$params["width"]}\" " : "";
        $height = isset($params["height"]) ? "height=\"{$params["height"]}\" " : "";

        echo "<div><img src=\"$file_upload_url/$filename\" {$width}{$height}/></div>";

        // if required, add the "Delete"
        if (!isset($params["hide_delete_button"]) || !$params["hide_delete_button"]) {
            $delete_file_label = (isset($params["delete_button_label"])) ? $params["delete_button_label"] : "Delete file";

            echo "<div><input type=\"submit\" name=\"form_tools_delete_image_field__$field_name\" value=\"$delete_file_label\" /></div>";
        }
    }


    /**
     * Just a wrapper function for General::loadField - renamed for consistency with the API. Plus it's good
     * to draw attention to this function with the additional documentation.
     *
     * @param string $field_name
     * @param string $session_name
     * @param string $default_value
     */
    public function loadField($field_name, $session_name, $default_value)
    {
        return General::loadField($field_name, $session_name, $default_value);
    }


    /**
     * This function lets you log a client or administrator in programmatically. By default, it logs the user in
     * and redirects them to whatever login page they specified in their user account. However, you can override
     * this in two ways: either specify a custom URL where they should be directed to, or avoid redirecting at
     * all. If you choose the latter, make sure you've initiated SESSIONS on the calling page - otherwise the
     * login account information (needed to be stored in sessions) is lost.
     *
     * @param array $info a hash with the following possible parameters:
     *     "username" - the username
     *     "password" - the password
     *     "auto_redirect_after_login" - (boolean, defaulted to false) determines whether or not the user should
     *         be automatically redirected to a URL after a successful login.
     *     "login_url" - the URL to redirect to (if desired). If this isn't set, but auto_redirect_after_login IS,
     *         it will log the user in normally, to whatever login page they've specified in their account.
     */
    public function login($info)
    {
        $password = isset($info["password"]) ? $info["password"] : "";

        if (empty($password)) {
            return self::processError(1000);
        }

        $account_info = Accounts::getAccountByUsername($info["username"]);

        if (empty($account_info)) {
            return self::processError(1004);
        }

        if ($account_info["account_status"] == "disabled") {
            return self::processError(1001);
        }

        if ($account_info["account_status"] == "pending") {
            return self::processError(1002);
        }

        if (General::encode($password) != $account_info["password"]) {
            return self::processError(1003);
        }

        return Core::$user->login($info);
    }


    /**
     * Creates a client account in the database.
     *
     * @param array $account_info this has has 4 required keys: first_name, last_name, user_name, password
     *
     * The password is automatically encrypted by this function.
     *
     * It also accepts the following optional keys:
     *     account_status: "active", "disabled", "pending"
     *     ui_language: (should only be one of the languages currently supported by the script, e.g. "en_us")
     *     timezone_offset: +- an integer value, for each hour
     *     sessions_timeout:
     *     date_format:
     *     login_page:
     *     logout_url:
     *     theme:
     *     menu_id:
     *
     * @return array [0] true / false
     *               [1] an array of error codes (if false) or the new account ID
     */
    public function createClientAccount($account_info)
    {
        $api_debug = Core::isAPIDebugEnabled();

        $error_codes = array();

        // check all the valid fields
        if (!isset($account_info["first_name"]) || empty($account_info["first_name"])) {
            $error_codes[] = 700;
        }
        if (!isset($account_info["last_name"]) || empty($account_info["last_name"])) {
            $error_codes[] = 701;
        }
        if (!isset($account_info["email"]) || empty($account_info["email"])) {
            $error_codes[] = 702;
        }
        if (!General::isValidEmail($account_info["email"])) {
            $error_codes[] = 703;
        }

        if (!isset($account_info["username"]) || empty($account_info["username"])) {
            $error_codes[] = 704;
        } else {
            if (preg_match('/[^A-Za-z0-9]/', $account_info["username"])) {
                $error_codes[] = 705;
            }
            if (!Accounts::isValidUsername($account_info["username"])) {
                $error_codes[] = 706;
            }
        }

        if (!isset($account_info["password"]) || empty($account_info["password"])) {
            $error_codes[] = 707;
        } else {
            if (preg_match('/[^A-Za-z0-9]/', $account_info["password"])) {
                $error_codes[] = 708;
            }
        }

        if (!empty($error_codes)) {
            if ($api_debug) {
                $page_vars = array("message_type" => "error", "error_codes" => $error_codes);
                Themes::displayPage("error.tpl", $page_vars);
                exit;
            } else {
                return array(false, $error_codes);
            }
        }

        // to pass validation
        $account_info["password_2"] = $account_info["password"];

        list ($success, $message, $new_user_id) = Administrator::addClient($account_info);

        if (!$success) {
            return self::processError(709, array(
                "debugging" => "Failed query in <b>" . __FUNCTION__ . "</b>: " . $message
            ));
        }

        return array(true, $new_user_id);
    }


    /**
     * Updates a client account with whatever values are in $info.
     *
     * @param integer $account_id
     * @param array $info an array of keys to update, corresponding to the columns in the accounts table.
     */
    public function updateClientAccount($account_id, $info)
    {
        $db = Core::$db;

        // check the account ID is valid
        $account_info = Accounts::getAccountInfo($account_id);

        // check the account ID was valid (i.e. the account exists) and that it's a CLIENT account
        if (!isset($account_info["account_id"])) {
            return self::processError(900);
        }
        if ($account_info["account_type"] != "client") {
            return self::processError(901);
        }

        // the list of DB columns that can be updated
        $valid_columns = array(
            "account_status",
            "ui_language",
            "timezone_offset",
            "sessions_timeout",
            "date_format",
            "login_page",
            "logout_url",
            "theme",
            "menu_id",
            "first_name",
            "last_name",
            "email",
            "username",
            "password"
        );

        $columns_to_update = array();
        while (list($key, $value) = each($info)) {
            // if something passed by the user isn't a valid column name, ignore it
            if (!in_array($key, $valid_columns)) {
                continue;
            }

            // if this is the password field, encrypt it
            if ($key == "password") {
                $value = General::encode($value);
            }

            $columns_to_update[$key] = $value;
        }

        if (empty($columns_to_update)) {
            return array(true, "");
        }

        $update_lines = $db->getUpdateStatements($columns_to_update);

        try {
            $db->query("
                UPDATE {PREFIX}accounts
                SET $update_lines
                WHERE account_id = :account_id
            ");
            $db->bindAll($columns_to_update);
            $db->bind("account_id", $account_id);
            $db->execute();
        } catch (Exception $e) {
            return self::processError(902, array(
                "Failed query in <b>" . __FUNCTION__ . ", " . __FILE__ . "</b>, line " . __LINE__ . ": " . $e->getMessage()
            ));
        }

        return array(true, "");
    }


    /**
     * Completely deletes a client account from the database.
     *
     * @param integer $account_id
     * @return mixed
     *        if success:
     *            returns array with two indexes: [0] true, [1] empty string
     *
     *        if error:
     *            if $api_debug == true
     *               the error page will be displayed displaying the error code.
     *
     *            if $api_debug == false, it returns an array with two indexes:
     *                   [0] false
     *                   [1] the API error code
     */
    public function deleteClientAccount($account_id)
    {
        $account_info = Accounts::getAccountInfo($account_id);

        // check the account ID was valid (i.e. the account exists) and that it's a CLIENT account
        if (!isset($account_info["account_id"])) {
            return self::processError(800);
        }
        if ($account_info["account_type"] != "client") {
            return self::processError(801);
        }

        Clients::deleteClient($account_id);

        return array(true, "");
    }


    /**
     * Deletes all unfinalized submissions and any associated files that have been uploaded.
     *
     * @param boolean $delete_all this deletes ALL unfinalized submissions. False by default. Normally it just
     *    deletes all unfinalized submissions made 2 hours and older. This wards against accidentally deleting
     *    those submissions currently being put through.
     *
     * @return mixed the number of unfinalized submissions that were just deleted, or error.
     */
    public function deleteUnfinalizedSubmissions($form_id, $delete_all = false)
    {
        $db = Core::$db;

        if (!Forms::checkFormExists($form_id)) {
            return self::processError(650);
        }

        $time_clause = (!$delete_all) ? "AND DATE_ADD(submission_date, INTERVAL 2 HOUR) < curdate()" : "";
        $db->query("
            SELECT *
            FROM   {PREFIX}form_{$form_id}
            WHERE  is_finalized = 'no'
            $time_clause
        ");
        $db->execute();
        $submissions = $db->fetchAll();

        $num_rows = $db->numRows();
        if ($num_rows == 0) {
            return 0;
        }

        // find out which of this form are file fields
        $form_fields = Fields::getFormFields($form_id);
        $file_type_id = FieldTypes::getFieldTypeIdByIdentifier("file");

        $file_field_info = array(); // a hash of col_name => file upload dir
        foreach ($form_fields as $field_info) {
            if ($field_info["field_type_id"] == $file_type_id) {
                $field_id = $field_info["field_id"];
                $col_name = $field_info["col_name"];
                $field_settings = Fields::getFieldSettings($field_id);
                $file_field_info[$col_name] = $field_settings["folder_path"];
            }
        }

        // now delete the info
        foreach ($submissions as $submission_info) {
            $submission_id = $submission_info["submission_id"];

            // delete any files associated with the submission
            foreach ($file_field_info as $col_name => $file_upload_dir) {
                if (!empty($submission_info[$col_name])) {
                    @unlink("{$file_upload_dir}/{$submission_info[$col_name]}");
                }
            }

            $db->query("DELETE FROM {PREFIX}form_{$form_id} WHERE submission_id = :submission_id");
            $db->bind("submission_id", $submission_id);
            $db->execute();
        }

        return $num_rows;
    }


    /**
     * Displays a captcha in your form pages, using the recaptcha service (http://recaptcha.net). Requires
     * you to have set up an account with them for this current website.
     *
     * @return mixed generally, this function just displays the CAPTCHA in your page. But in case of error:
     *
     *        if $api_debug == true, the error page will be displayed displaying the error code.
     *        if $api_debug == false, it returns an array with two indexes:
     *                   [0] false
     *                   [1] the API error code
     */
    public function displayCaptcha($show_error = true)
    {
        $recaptcha_site_key = Core::getApiRecaptchaSiteKey();
        $recaptcha_secret_key = Core::getApiRecaptchaSecretKey();
        $recaptcha_lang = Core::getApiRecaptchaLang();

        // check the two recaptcha keys have been defined
        if (empty($recaptcha_site_key) || empty($recaptcha_secret_key)) {
            self::displayInPageErrorBlock(600);
            exit;
        }

        $this->includeRecaptchaLib();

        if (isset($GLOBALS["g_api_recaptcha_error"]) && $show_error) {
            echo <<< END
            <div class="form_tools_recaptcha_error">
                There was a reCAPTCHA error.
            </div>
END;
        }

echo <<< END
<div class="g-recaptcha" data-sitekey="{$recaptcha_site_key}"></div>
<script type="text/javascript" src="https://www.google.com/recaptcha/api.js?hl={$recaptcha_lang}"></script>
END;
    }


    /**
     * This function checks to see if a submission is unique - based on whatever criteria you require
     * for your test case.
     *
     * @param integer $form_id
     * @param array $criteria a hash of whatever criteria is need to denote uniqueness, where the key is the
     *   database column name and the value is the current value being tested. For instance, if you wanted to check
     *   that no-one has submitted a form with a particular email address, you could pass
     *   array("email" => "myemail@whatever.com) as the second parameter (where "email" is the database column name).
     * @param integer $current_submission_id if this value is set, the function ignores that submission when doing
     *   a comparison.
     */
    public function checkSubmissionIsUnique($form_id, $criteria, $current_submission_id = "")
    {
        $db = Core::$db;

        // confirm the form is valid
        if (!Forms::checkFormExists($form_id)) {
            return self::processError(550);
        }

        if (!is_array($criteria)) {
            return self::processError(551);
        }

        $where_clauses = array();
        $placeholders = array();
        while (list($col_name, $value) = each($criteria)) {
            if (empty($col_name)) {
                return self::processError(552);
            }

            $where_clauses[] = "$col_name = :$col_name";
            $placeholders[$col_name] = $value;
        }

        if (empty($where_clauses)) {
            return self::processError(553);
        }

        if (!empty($current_submission_id)) {
            $where_clauses[] = "submission_id != :submission_id";
            $placeholders["submission_id"] = $current_submission_id;
        }

        $where_clause_str = "WHERE " . join(" AND ", $where_clauses);
        try {
            $db->query("
                SELECT count(*)
                FROM {PREFIX}form_{$form_id}
                $where_clause_str
            ");
            $db->bindAll($placeholders);
            $db->execute();
        } catch (Exception $e) {
            return self::processError(554);
        }

        return $db->fetch(PDO::FETCH_COLUMN) === 0;
    }


    /**
     * This function was provided to allow POST form users to include a reCAPTCHA in their form. This is
     * used to display an error message in the event of a failed attempt.
     *
     * @param string $message the message to output if there was a problem with the CAPTCHA contents.
     */
    public function displayPostFormCaptchaError($message = "")
    {
        if (!isset($_SESSION["form_tools_form_data"])) {
            return;
        }

        if (isset($_SESSION["form_tools_form_data"]["api_recaptcha_error"]) && !empty($_SESSION["form_tools_form_data"]["api_recaptcha_error"])) {
            if ($message) {
                echo $message;
            } else {
                echo "Sorry, the CAPTCHA (image verification) was entered incorrectly. Please try again.";
            }
        }

        $_SESSION["form_tools_form_data"]["api_recaptcha_error"] = "";
    }


    /**
     * Returns all information about a submission. N.B. Would have been nice to have made this just a
     * wrapper for ft_get_submission_info, but that function contains hooks. Need to revise all core
     * code to allow external calls to optionally avoid any hook calls.
     *
     * @param integer $form_id
     * @param integer $submission_id
     */
    public function getSubmission($form_id, $submission_id)
    {
        $db = Core::$db;

        // confirm the form is valid
        if (!Forms::checkFormExists($form_id)) {
            return self::processError(405);
        }

        if (!is_numeric($submission_id)) {
            return self::processError(406);
        }

        // get the form submission info
        $db->query("
            SELECT *
            FROM   {PREFIX}form_{$form_id}
            WHERE  submission_id = :submission_id
        ");
        $db->bind("submission_id", $submission_id);
        $db->execute();

        return $db->fetch();
    }


    public function includeRecaptchaLib()
    {
        require_once(__DIR__ . "/recaptcha/autoload.php");
    }


    public function validateRecaptcha($g_recaptcha_response)
    {
        $secret_key = Core::getAPIRecaptchaSecretKey();
        $this->includeRecaptchaLib();

        $recaptcha = new \ReCaptcha\ReCaptcha($secret_key);
        return $recaptcha->verify($g_recaptcha_response, $_SERVER['REMOTE_ADDR']);
    }


    /**
     * Called for all API errors. If `$g_api_debug = true;` is set in the users' config.php file, this method
     * redirects the user to a webpage containing details about the error, plus a link
     * @param $error_code
     * @param array $extra_info
     * @return array
     */
    private static function processError($error_code, $extra_info = array())
    {
        $api_debug = Core::isAPIDebugEnabled();

        if ($api_debug) {
            self::displayError($error_code, $extra_info);
            exit;
        } else {
            return array(false, $error_code);
        }
    }

    private static function displayError($error_code, $extra_info = array())
    {
        $is_system_error = in_array($error_code, self::$systemErrors);

        $content = array_merge(array(
            "error_code" => $error_code,
            "message_type" => "error",
            "error_type" => $is_system_error ? "system" : "user"
        ), $extra_info);

        Themes::displayPage("error.tpl", $content);
    }


    private static function displayInPageErrorBlock($error_code)
    {
        $LANG = Core::$L;

        echo <<< END
<div style="padding: 8px; margin: 8px; background-color: #f2dede; border: 1px solid #ebccd1; border-radius: 4px; color: #a94442; display: inline-block; font-family: arial; font-size: 13px; ">
    <b>Form Tools API Error:</b> $error_code - <a href="https://docs.formtools.org/api/v2/error_codes/#{$error_code}">{$LANG["phrase_error_learn_more"]}</a>
</div>
END;
    }
}
