Skip to content
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
## ignore our config file. See config.php.sample for a blank

includes/config.php
includes/errorlog.txt
.gitignore~
.htaccess~

quizzes/

.idea
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,24 @@
## A quiz builder and formative assessment tool for legal education

CALI QuizWright is a tool for creating and publishing formative assessments for your class. QuizWright gives law teachers a web-based platform to create and manage personal question banks and quizzes for their students. When used with CALI's AutoPublish, LessonLive, and LessonLink features, QuizWright quizzes can be delivered to students and taken in real time in the classroom using the familiar CALI Lesson Viewer.

## simpleSAMLphp service provider

CALI Author uses simpleSAMLphp as its SAML service provider and authenticates using the CALI website as the SAML identity provider.

This integration expects QuizWright to be installed in a folder named `quizwright` in the project root folder (not `web/`!) of the `cali-drupal` codebase. The Apache virtual host for Drupal should include the following to make `quizwright` available from this location:

```apacheconf
# QuizWright configuration
Alias /quizwright /path/to/drupal/quizwright

<Directory /path/to/drupal/quizwright>
Options FollowSymLinks
AllowOverride None
Require all granted
</Directory>
```

All of simpleSAMLphp's custom configurations for the CALI-Author SAML SP are in the `cali-drupal` codebase.

For QuizWright to use the service provider, the `USER_MGMNT` constant in `includes/config.php` should point to `includes/saml_user.php`.
27 changes: 13 additions & 14 deletions includes/config.php.default
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,28 @@
*/

// db options
define('DB_NAME', 'your db name');
define('DB_USER', 'your db username');
define('DB_PASSWORD', 'your db password');
define('DB_HOST', 'localhost');
const DB_NAME = 'quizwright';
const DB_USER = 'db';
const DB_PASSWORD = 'db';
const DB_HOST = 'db';

$mysqli = new mysqli(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME);

// full path to user management
define('USER_MGMNT', '/local/var/www/caw/local_user.php');
define('DRUPAL_REGISTER', 'http://your.drupal/regsiter/page');
const USER_MGMNT = '/var/www/html/quizwright/includes/saml_user.php';
const DRUPAL_REGISTER = 'http://your.drupal/regsiter/page';

// define connection info for user info source.
// could be another db or something like LDAP
define('UDB_NAME', 'user db');
define('UDB_USER', 'user db username');
define('UDB_PASSWORD', 'user db password');
define('UDB_HOST', 'localhost');
const UDB_NAME = 'user db';
const UDB_USER = 'user db username';
const UDB_PASSWORD = 'user db password';
const UDB_HOST = 'localhost';

$umysqli = new mysqli(UDB_HOST,UDB_USER,UDB_PASSWORD,UDB_NAME);
//$umysqli = new mysqli(UDB_HOST,UDB_USER,UDB_PASSWORD,UDB_NAME);

//full URL of your site
define('SITE_URL', 'http://where.is.my.site/')
const SITE_URL = https://cali-d10.ddev.site/quizwright/';

//where to publish quizzes
define('PUBLISH_URL','http://where.is.my.publish.endpoint/')
?>
const PUBLISH_URL = 'https://cali-d10.ddev.site/publisher/publish/qw';
2 changes: 1 addition & 1 deletion includes/home.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<li><a id="profile-edit" href="#">My Profile</a></li>
</ul>
</li>
<li><a href="?u=logout"><i class="glyphicon glyphicon-lock"></i> Logout</a></li>
<li><a href="logout.php"><i class="glyphicon glyphicon-lock"></i> Logout</a></li>
</ul>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions includes/page-update.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
//error_reporting(E_ALL);
require ("user-session.php");
require ("utility.php");
$pid = $_POST['pid'];
if (!isset($pid))
$pid = $_POST['pid'] ?? null;
if (empty($pid))
{ // No ID? Must be a brand new page.
if ($_POST['page-type'] != '')
{
Expand Down
22 changes: 11 additions & 11 deletions includes/profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
$sql = "SELECT profile FROM `people` WHERE uid = '$uid'";
if ($result = $mysqli->query($sql)) {
while ($row = $result->fetch_assoc()) {
$data = json_decode($row['profile'], TRUE);
$data = json_decode($row['profile'] ?? '{}', TRUE) ?: [];
?>


Expand All @@ -28,44 +28,44 @@
<div class="form-group">
<label class="col-sm-2 control-label" for="authorfullname">Author Name</label>
<div class="col-sm-8">
<input id="authorfullname" name="authorfullname" placeholder="Your full name here" class="form-control" type="text" value="<?=$data['authorfullname']?>">
<input id="authorfullname" name="authorfullname" placeholder="Your full name here" class="form-control" type="text" value="<?=$data['authorfullname'] ?? ''?>">

</div>
</div>

<!-- Text input-->
<div class="form-group">
<label class="col-sm-2 control-label" for="authortitle">Author title</label>
<div class="col-sm-8">
<input id="authortitle" name="authortitle" placeholder="Your title" class="form-control" type="text" value="<?=$data['authortitle']?>">
<input id="authortitle" name="authortitle" placeholder="Your title" class="form-control" type="text" value="<?=$data['authortitle'] ?? ''?>">

</div>
</div>

<!-- Text input-->
<div class="form-group">
<label class="col-sm-2 control-label" for="authorschool">School</label>
<div class="col-sm-8">
<input id="authorschool" name="authorschool" placeholder="Your school" class="form-control" type="text" value="<?=$data['authorschool']?>">
<input id="authorschool" name="authorschool" placeholder="Your school" class="form-control" type="text" value="<?=$data['authorschool'] ?? ''?>">

</div>
</div>

<!-- Text input-->
<div class="form-group">
<label class="col-sm-2 control-label" for="authoremail">Email</label>
<div class="col-sm-8">
<input id="authoremail" name="authoremail" placeholder="Your email address" class="form-control" type="text" value="<?=$data['authoremail']?>">
<input id="authoremail" name="authoremail" placeholder="Your email address" class="form-control" type="text" value="<?=$data['authoremail'] ?? ''?>">

</div>
</div>

<!-- Text input-->
<div class="form-group">
<label class="col-sm-2 control-label" for="authorphone">Phone</label>
<div class="col-sm-8">
<input id="authorphone" name="authorphone" placeholder="234-456-7890" class="form-control" type="text" value="<?=$data['authorphone']?>">
<input id="authorphone" name="authorphone" placeholder="234-456-7890" class="form-control" type="text" value="<?=$data['authorphone'] ?? ''?>">

</div>
</div>

Expand Down
4 changes: 2 additions & 2 deletions includes/quiz-create.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
$data = json_encode($_POST);

// Old, bad quotes: $mysqli->query("INSERT INTO info (lid,uid,data) VALUES ('',$uid,'$data')");
$stmt = mysqli_prepare($mysqli, "INSERT INTO info (lid,uid,data) VALUES (?,?,?)");
mysqli_stmt_bind_param($stmt, "iis", $lid, $uid, $data);
$stmt = mysqli_prepare($mysqli, "INSERT INTO info (uid,data) VALUES (?,?)");
mysqli_stmt_bind_param($stmt, "is", $uid, $data);
mysqli_stmt_execute($stmt);
$result=mysqli_connect_error();
$lid = $mysqli->insert_id;
Expand Down
115 changes: 115 additions & 0 deletions includes/saml_user.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php
/** @noinspection SqlNoDataSourceInspection */
/** @noinspection SqlResolve */
/** @noinspection PhpConditionAlreadyCheckedInspection */
/**
* SAML-based user management
* Uses SimpleSAMLphp installed via Composer
*/

// Load config (for SITE_URL and other constants)
require_once(__DIR__ . '/config.php');

// Load Composer's autoloader from parent project (loads SimpleSAMLphp and all dependencies)
use SimpleSAML\Auth\Simple;
use SimpleSAML\Configuration;

require_once(__DIR__ . '/../../vendor/autoload.php');

// SimpleSAMLphp config is set via SIMPLESAMLPHP_CONFIG_DIR environment variable
// No need to call Configuration::setConfigDir()

function userLoginViaSAML(): string
{
global $mysqli;

$as = new Simple('quizwright-sp');

if (!$as->isAuthenticated()) {
return "User not authenticated via SAML.";
}

$attributes = $as->getAttributes();

// Use 'cn' (common name) as the username, fallback to 'uid' if not available
$name = $attributes['cn'][0] ?? $attributes['uid'][0] ?? $attributes['username'][0] ?? null;
$email = $attributes['mail'][0] ?? $attributes['email'][0] ?? null;
$roles = $attributes['roles'] ?? [];

// Get the Drupal UID for reference
$drupal_uid = $attributes['uid'][0] ?? null;

if (!$name) {
return "Missing username from SAML assertion.";
}

// Check for required role - QuizWright requires CALI Staff or facstaff role
$hasRequiredRole = false;
$CALIStaff = false;
$requiredRoles = ['cali_staff', 'facstaff'];

foreach ($roles as $role) {
if (in_array($role, $requiredRoles)) {
$hasRequiredRole = true;
// Check if user is CALI Staff
if ($role === 'cali_staff') {
$CALIStaff = true;
}
break;
}
}

if (!$hasRequiredRole) {

return "User does not have required role. QuizWright requires a CALI member faculty/staff account.";
}

$stmt = $mysqli->prepare("SELECT * FROM `people` WHERE username = ?");
$stmt->bind_param("s", $name);
$stmt->execute();
$result = $stmt->get_result();
$count = $result->num_rows;

if ($count == 1) {
$row = $result->fetch_array(MYSQLI_ASSOC);
$_SESSION['username'] = $row['username'];
$_SESSION['uid'] = $row['uid'];
$_SESSION['CALIStaff'] = $CALIStaff;
$err = "";
} else {
$data = json_encode($attributes);
// SAML users don't have passwords - use empty string
$password = '';
$stmt = $mysqli->prepare("INSERT INTO `people` (username, email, password, data) VALUES (?, ?, ?, ?)");
$stmt->bind_param("ssss", $name, $email, $password, $data);

if ($stmt->execute()) {
$uid = $mysqli->insert_id;
$_SESSION['username'] = $name;
$_SESSION['uid'] = $uid;
$_SESSION['CALIStaff'] = $CALIStaff;
$err = "";
} else {
$err = sprintf("Error: %s\n", $mysqli->error);
}
}

return $err;
}

function userLogoutViaSAML(): void
{
// Local logout only - does NOT log out of IdP (Drupal)
// Just destroy the local PHP session
session_unset();
session_destroy();

// Clear SimpleSAMLphp auth data from session without triggering SAML logout
if (isset($_COOKIE['SimpleSAMLAuthToken'])) {
setcookie('SimpleSAMLAuthToken', '', time() - 3600, '/', '', false, true);
}

// Redirect back to the app login page
header('Location: ' . (SITE_URL ?? '/quizwright/'));
exit;
}
2 changes: 2 additions & 0 deletions includes/user-session.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php
require ("./config.php");
// Use SimpleSAML session (same as index.php)
session_name('SimpleSAML');
session_start();
$uid = $_SESSION['uid'];
?>
59 changes: 55 additions & 4 deletions index.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
<?php
date_default_timezone_set("America/Chicago");

require_once(__DIR__ . '/includes/config.php');

// Use SimpleSAML session
session_name('SimpleSAML');
session_start();
require ("./includes/config.php");

// Check if user is authenticated
$isAuthenticated = isset($_SESSION['uid']) && $_SESSION['uid'] > 0;

if (!$isAuthenticated) {
// Show login page instead of the app
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CALI QuizWright - Login Required</title>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet">
<style>
.login-container {
max-width: 500px;
margin: 100px auto;
padding: 30px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="login-container">
<h2>CALI QuizWright</h2>
<p>QuizWright is a web app that lets teachers write individual MC, T/F, Y/N questions,
saves the questions in a personal question bank, allows teachers to bundle the questions
into quizzes, turns the quizzes into AutoPublish Lessons that are published to the CALI
website and run by students either as LessonLive or LessonLink assessments.</p>
<p><strong>All you need to use QuizWright is a CALI member faculty/staff account.</strong></p>
<a href="login.php?return=<?php echo urlencode($_SERVER['REQUEST_URI'] ?? '/quizwright/'); ?>" class="btn btn-primary btn-lg">
Log in with your CALI Account
</a>
</div>
</div>
</body>
</html>
<?php
exit;
}

// User is authenticated, continue with normal functionality
if (isset($_POST["lesson-submit"])){
$data = json_encode($_POST);
$uid = $_SESSION['uid'];
Expand All @@ -23,10 +73,11 @@
<![endif]-->
<link href="css/styles.css" rel="stylesheet">
</head>
<body>
<body>
<?php
// load user control
include(USER_MGMNT);
// Load the main UI for authenticated users
$username = $_SESSION['username'];
include('./includes/home.php');
?>
<footer class="text-center"><div id="footer-wrapper">
<div ><img src="images/CALI_LogoTagline_DarkGrayMedium.png" /></div>
Expand Down
Loading