* @version 1.0 * @since 20 Aug 2014 17:20:09 */ class Model { // The database fields used by this model (this should be overwritten by each subclass). public static $fields = array(); // The Model::get() method requires a $limit value to return a list. If we want to return "all items" the only way is to set the value to a very high number. So we use this constant as that value. const INNUMERABLE = 999999999; /* * * * @access public * @param * @return * @author Quinn Comendant * @version 1.0 * @since 17 Nov 2014 22:16:51 */ static public function merge() { $merged = static::$fields; foreach (func_get_args() as $a) { if (!is_array($a)) { $app->logMsg(sprintf('Merge arguments must be arrays; %s is not an array.', (string)$a), LOG_NOTICE, __FILE__, __LINE__); return false; } $merged = array_merge($merged, $a); } return $merged; } /* * Returns rows of the model's data, filtered by an array of parameters. * * @access public * @param array $filters Array of key-value pairs matching the model's fields. * @param mixed $limit Quantity of values to return (if integer), or one unindexed value (if null). To use this method to return a list, a value must be set here. * @param int $offset The offset of the first row to return, useful for pagination. * @return array An associative array result set returned from the database. * @author Quinn Comendant * @version 1.0 * @since 18 Nov 2014 15:56:09 */ static public function get($filters=array(), $limit=null, $offset=0) { $app =& App::getInstance(); $db =& DB::getInstance(); static $results = array(); if (!is_array($filters) || sizeof($filters) != sizeof(array_intersect(array_keys(static::$fields), array_keys($filters)))) { $app->logMsg(sprintf('Invalid filters given: %s (not an array or db column mismatch)', getDump($filters)), LOG_ERR, __FILE__, __LINE__); return false; } // Do something clever so we don't hit the database repeatedly for the same query during one request. ksort($filters); $hash = md5(json_encode($filters) . '|' . $limit . '|' . $offset); if (isset($results[$hash])) { return is_null($limit) && isset($results[$hash][0]) ? $results[$hash][0] : $results[$hash]; } // Convert the filters into a SQL where clause. $where_clause = ''; foreach ($filters as $k => $v) { $where_clause .= sprintf("%s%s = '%s'", ('' == $where_clause ? 'WHERE ' : ' AND '), $db->escapeString($k), $db->escapeString($v) ); } // Submit the query, including limit and offset. $qid = $db->query(sprintf('SELECT * FROM %s_tbl %s LIMIT %s,%s', // Table name derived from [class name]_tbl. $db->escapeString(strtolower(get_called_class())), // WHERE … AND … AND … $where_clause, // "LIMIT 0,1" if limit undefined, or use given numeric limit and offset. $db->escapeString($offset), (is_null($limit) ? '1' : $db->escapeString($limit)) )); // Load the results. $results[$hash] = array(); while ($row = mysql_fetch_assoc($qid)) { $results[$hash][] = $row; } $app->logMsg(sprintf('%s::get found %s items for filter: %s', get_called_class(), sizeof($results[$hash]), getDump($filters)), LOG_DEBUG, __FILE__, __LINE__); // If no limit is specified, and a first row exists, return it unindexed. Otherwise, return the full result set from mysql_fetch_assoc (which may not be an array). return is_null($limit) && isset($results[$hash][0]) ? $results[$hash][0] : $results[$hash]; } /* * 1. Request delete action on items matching key => value 2. Model::get() returns 10 items matching this filter 3. Loop through items and compares account_id of each with the user's account_id 4. If any account_id mismatch, deny access, otherwise allow. 5. User is able to delete 10 items if all items belong to their account. 6. User is able to delete 0 items if any item does not belong to their account. * * @access public * @param * @return * @author Quinn Comendant * @version 1.0 * @since 28 Nov 2014 12:28:10 */ static public function checkAccess($action, $item_rows=null) { global $acl, $auth; $app =& App::getInstance(); $aro = sprintf('user_id:%s', $auth->get('user_id')); $aco = sprintf('%s:%s', strtolower(get_called_class()), $action); // Basic ACL check for user -> class:action. if (!$acl->check($aro, $aco)) { // DENY: User is not allowed this action. $app->logMsg(sprintf('Action "%s" denied for user_id "%s" by ACL', $aco, $auth->get('user_id')), LOG_WARNING, __FILE__, __LINE__); return false; } // Check if the user is allowed this action on 'any' item. if ($acl->check($aro, $aco, 'any')) { // ALLOW: User has access to 'any' item. $app->logMsg(sprintf('Action "%s" allowed for user_id "%s" on ANY items', $aco, $auth->get('user_id')), LOG_INFO, __FILE__, __LINE__); return true; } // If no item rows, there is nothing being acted upon anyway. // This clause triggers when item not found or using this function only for its first acl->check(…) block. if (!is_array($item_rows) || empty($item_rows)) { // ALLOW: Knock yerself out. $app->logMsg(sprintf('Action "%s" allowed for user_id "%s" with no items', $aco, $auth->get('user_id')), LOG_NOTICE, __FILE__, __LINE__); return true; } // Check that all the given items belong to the user's account. ($item_rows was confirmed to be an array in the previous block.) foreach ($item_rows as $row) { // We're only comparing if the item's account_id == the logged-in user's account_id. if (!isset($row['account_id']) || empty($row['account_id']) || $auth->get('account_id') != $row['account_id']) { // DENY: An item is not owned by the user. $app->logMsg(sprintf('Action "%s" denied for user_id "%s" account_id mismatch in rows: %s', $aco, $auth->get('user_id'), getDump($row)), LOG_WARNING, __FILE__, __LINE__); return false; } } // ALLOW: All items are owned by the user. $app->logMsg(sprintf('Action "%s" allowed for user_id "%s" on %s items', $aco, $auth->get('user_id'), sizeof($item_rows)), LOG_INFO, __FILE__, __LINE__); return true; } /* * * * @access public * @param * @return * @author Quinn Comendant * @version 1.0 * @since 29 Nov 2014 23:25:19 */ static public function requireAllow($action, $item_rows=null, $message='', $type=MSG_NOTICE, $file=null, $line=null) { global $auth, $locally_carried_queries; $app =& App::getInstance(); if (!self::checkAccess($action, $item_rows)) { // I'm sorry Hal, I couldn't resist… $dave = ('' != $auth->get('first_name') ? $auth->get('first_name') : ('' != $auth->get('username') ? $auth->get('username') : 'Dave')); $message = '' == trim($message) ? sprintf(_("I’m sorry, %s. I'm afraid I can’t do that."), $dave) : $message; $app->raiseMsg($message, $type, $file, $line); $carried_queries = $locally_carried_queries ? $locally_carried_queries : false; // TODO: Do we want to strip queries? This would only be for aesthetic purposes… $app->dieBoomerangURL(strtolower(get_called_class()), $carried_queries); } } }