Intercepting ODK Collect submission with PHP

Open Data Kit is a great open source platform for mobile data collection. However, one of its major downsides is that there are no configurations possible to limit which user has access to which form and to which data. In short, every user on a given ODK Aggregate install has access to every form and every associated dataset on the server. Since this was inappropriate for our needs, we decided to create a layer of php files that intercept requests and submissions from ODK Collect and redirect them to ODK Aggregate.

Through this approach, we were able to only show users the forms they created.

The following code snippet is for an index.php file that needs to be in a /formList folder on the server specified as the Aggregate server in Collect.

$error = false;
$elog = "";

if (empty($_SERVER['PHP_AUTH_DIGEST'])){ // before credentials

        http_response_code(401);
        header("HTTP/1.1 401 Unauthorized");
        header('Content-Type: text/xml; charset=utf-8');
        header('WWW-Authenticate: Digest realm="**INSERT YOUR REALM HERE**",qop="auth",nonce="'.uniqid().'",opaque="'.md5('**INSERT YOUR REALM HERE**').'"');
        header('"HTTP_X_OPENROSA_VERSION": "1.0"');
    header('X-OpenRosa-Version:1.0');

} else { // after credentials

        header('Content-Type: text/xml; charset=utf-8');
    header('"HTTP_X_OPENROSA_VERSION": "1.0"');
    header('X-OpenRosa-Version:1.0');

    $uid=check_user_pass();
    if ($uid!==false){
        echo '<xforms xmlns="http://openrosa.org/xforms/xformsList">';
        $uid = pg_escape_literal($uid);
        $query = 'SELECT * FROM odk_prod._form_info WHERE "_CREATOR_URI_USER" = '.$uid.' AND "_IS_COMPLETE" = TRUE';
        $result = pg_query($con_pg, $query);
        if (!$result) {
            $error = true;
        } else {
            while ($row = pg_fetch_assoc($result)) {
                $form = $row["FORM_ID"];
                $query = 'SELECT "FORM_NAME", "ROOT_ELEMENT_MODEL_VERSION" FROM odk_prod._form_info_fileset WHERE "_TOP_LEVEL_AURI" = \''.$row['_URI'].'\'';
                $result_ref = pg_query($con_pg,$query);
                $row_ref=pg_fetch_assoc($result_ref);
                echo '<xform>';
                echo '<formID>'.$form.'</formID>';
                echo '<name>'.$row_ref['FORM_NAME'].'</name>';
                echo '<majorMinorVersion>'.$row_ref['ROOT_ELEMENT_MODEL_VERSION'].'</majorMinorVersion>';
                echo '<version>'.$row_ref['ROOT_ELEMENT_MODEL_VERSION'].'</version>';
                echo '<hash>'.$row['_URI'].'</hash>';
                echo '<downloadUrl>http://**YOUR_SERVER_ADDRESS**/odk/formXML/?formId='.$form.'</downloadUrl>';
                $query = 'SELECT * FROM odk_prod._form_info_manifest_bin WHERE "_TOP_LEVEL_AURI" = \''.$row['_URI'].'\'';
                $result2 = pg_query($con_pg, $query);
                if (pg_num_rows($result2)>0){
                echo '<manifestUrl>http://**YOUR_SERVER_ADDRESS**/formList/xformsManifest.php?formId='.$form.'</manifestUrl>';
                }
                echo '</xform>';
            }
        }
        echo '</xforms>';
    }
 }


function check_user_pass() {
  $data = http_digest_parse($_SERVER['PHP_AUTH_DIGEST']);
    $name = $data['username'];
    $error=false;
    // double check connection
    if (!$con_pg) {
        $error = true;
    }

    // query db for hashed password for given username
    if (!$error){
        $name = pg_escape_literal($name);
        $query = 'SELECT * FROM odk_prod._registered_users WHERE "LOCAL_USERNAME" = '.$name.' AND "IS_REMOVED" = FALSE';
        $result = pg_query($con_pg, $query);
        if (!$result) {
            $error = true;
        } else {
            while ($row = pg_fetch_assoc($result)) {
              $pwd = $row["DIGEST_AUTH_PASSWORD"]; //hashed password
              $uid = $row["_URI"]; //uid used in other tables
            }
        }
    }

    if (!$error){
        $A1 = $pwd;
        $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']);
        $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2);
        if ($data['response'] != $valid_response){
            $error = true;
        }
    }
    if (!$error){
        return $uid;
    }else{
        return false;


    }
}

For intercepting submissions from ODK Collect, the tricky part concerns the http response codes used by ODK Collect. First, a request is made by collect to the server, which responds with a 401 response code. Then, Collect sends the DIGEST AUTH information in the header and if the credentials are valid, the server needs to respond with a 204 response code AND specify a location in the header. The following code snippet achieves this part of the dialog.

if (!empty($_FILES)){

/// PROCESS THE $_FILES DATA HERE

}else{
    if (empty($_SERVER['PHP_AUTH_DIGEST'])) { // before credentials
        http_response_code(401);
        header("HTTP/1.1 401 Unauthorized");
        header('Content-Type: text/xml; charset=utf-8');
        header('WWW-Authenticate: Digest realm="YOUR REALM ODK Aggregate",qop="auth",nonce="'.uniqid().'",opaque="'.md5('YOUR REALM ODK Aggregate').'"');
        header('"HTTP_X_OPENROSA_VERSION":"1.0"');
        header('X-OpenRosa-Accept-Content-Length: 20000');
        header('Content-Length: 0');
    } else { // after credentials
        header("Server: Apache-Coyote/1.1");
        header("X-OpenRosa-Version: 1.0");
        header("X-OpenRosa-Accept-Content-Length: 1048576000");
        $uid=check_user_pass(); // Custom function to validate user name and password
        if($uid !== false){
            header("Location: http://***YOURSERVER***/submission/",true,204);
        }else{
            http_response_code(401);
        }
    }
}