3 * An object representing a 'Principal' read from the database
6 * @subpackage Principal
7 * @author Andrew McMillan <andrew@mcmillan.net.nz>
8 * @copyright Morphoss Ltd <http://www.morhposs.com/>
9 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
12 require_once('AwlCache.php');
15 * A class for things to do with a Principal
22 * Some control over our DB
25 private static $db_tablename = 'dav_principal';
26 private static $db_mandatory_fields = array(
30 public static function updateableFields() {
32 'username', 'email', 'user_active', 'modified', 'password', 'fullname',
33 'email_ok', 'date_format_type', 'locale', 'type_id', 'displayname', 'default_privileges'
38 * We cache these so if we try and access a row by principal_id/user_no/e_mail that we've
39 * already read we don't read it again.
42 private static $byUserno = array();
43 private static $byId = array();
44 private static $byEmail = array();
47 * Columns from the database
51 protected $principal_id;
60 public $date_format_type;
64 public $default_privileges;
67 public $collection_id;
68 public $is_addressbook;
69 public $resourcetypes;
73 * Whether this Principal actually exists in the database yet.
79 * @var The home URL of the principal
84 * @var The actual requested URL for this principal, when the request was for /principals/... or such
86 protected $original_request_url;
89 * Whether this was retrieved using an e-mail address
95 * If we're using memcached this is the namespace we'll put stuff in
101 protected $collections;
102 protected $dead_properties;
103 protected $default_calendar;
106 * Construct a new Principal object. The principal record will be retrieved from the database, or (if not found) initialised to a new record. You can test for whether the Principal exists by calling the Exists() method on the returned object.
108 * Depending on the supplied $type, the following behaviour will occur:
109 * path: Will attempt to extract a username or email from the supplied path, and then do what those do.
110 * dav_name: Expects the dav_name of a <em>principal</em>, exactly, like: /principal/ and will use that as for username.
111 * user_no: Expects an integer which is the usr.user_no (deprecated)
112 * principal_id: Expects an integer which is the principal.principal_id
113 * email: Will try and retrieve a unique principal by using the email address. Will fail (subsequent call to Exists() will be false) if there is not a unique match.
114 * username: Will retrieve based on strtolower($value) = lower(usr.username)
116 * @param string $type One of 'path', 'dav_name', 'user_no', 'principal_id', 'email' or 'username'
117 * @param mixed $value A value appropriate to the $type requested.
118 * @param boolean $use_cache Whether to use an available cache source (default true)
119 * @throws Exception When provided with an invalid $type parameter.
122 function __construct( $type, $value, $use_cache=true ) {
125 $this->exists
= false;
126 $this->by_email
= false;
127 $this->original_request_url
= null;
132 $value = $this->usernameFromPath($value);
136 $value = substr($value, 1, -1);
142 * There are some values we can construct on the basis of the constructor value.
145 case 'user_no': $this->user_no
= $value; break;
146 case 'principal_id': $this->principal_id
= $value; break;
147 case 'email': $this->email
= $value; break;
148 case 'username': $this->username
= $value; break;
150 throw new Exception('Can only retrieve a Principal by user_no,principal_id,username or email address');
153 $cache = new AwlCache();
154 if ( $use_cache && isset($session->principal_id
) ) {
157 if ( isset(self
::$byUserno[$value]) ) {
159 $value = self
::$byUserno[$value];
163 if ( isset(self
::$byId[$value]) ) {
165 $value = self
::$byId[$value];
169 $this->by_email
= true;
170 if ( isset(self
::$byEmail[$value]) ) {
172 $value = self
::$byEmail[$value];
177 if ( $type == 'username' ) {
178 $this->username
= $value;
179 $this->dav_name
= '/'.$value.'/';
180 $this->url
= ConstructURL( $this->dav_name
, true );
181 $this->cacheNs
= 'principal-/'.$value.'/';
182 $this->cacheKey
= 'p-'.$session->principal_id
;
183 $row = $cache->get('principal-/'.$value.'/', 'p-'.$session->principal_id
);
184 if ( $row !== false ) {
185 self
::$byId[$row->principal_id
] = $row->username
;
186 self
::$byUserno[$row->user_no
] = $row->username
;
187 self
::$byEmail[$row->email
] = $row->username
;
188 $this->assignRowValues($row);
189 $this->url
= ConstructURL( $this->dav_name
, true );
190 $this->exists
= true;
197 if ( isset($session->principal_id
) && $session->principal_id
!== false ) {
198 $sql .= 'pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS privileges ';
199 $params = array( ':session_principal' => $session->principal_id
, ':scan_depth' => $c->permission_scan_depth
);
202 $sql .= '0::BIT(24) AS privileges ';
205 $sql .= 'FROM dav_principal WHERE ';
208 $sql .= 'lower(username)=lower(text(:param))';
211 $sql .= 'user_no=:param';
214 $sql .= 'principal_id=:param';
217 $this->by_email
= true;
218 $sql .= 'lower(email)=lower(:param)';
221 $params[':param'] = $value;
223 $qry = new AwlQuery( $sql, $params );
224 if ( $qry->Exec('Principal',__LINE__
,__FILE__
) && $qry->rows() == 1 && $row = $qry->Fetch() ) {
225 $this->exists
= true;
226 if ( isset($session->principal_id
) ) {
227 self
::$byId[$row->principal_id
] = $row->username
;
228 self
::$byUserno[$row->user_no
] = $row->username
;
229 self
::$byEmail[$row->email
] = $row->username
;
230 if ( !isset($this->cacheNs
) ) {
231 $this->cacheNs
= 'principal-'.$row->dav_name
;
232 $this->cacheKey
= 'p-'.$session->principal_id
;
235 $this->assignRowValues($row);
236 $this->url
= ConstructURL( $this->dav_name
, true );
237 $row = $cache->set($this->cacheNs
, $this->cacheKey
, $row, 864000 );
241 if ( $type == 'username' && $value == 'unauthenticated' ) {
242 $this->assignGuestValues();
247 * This will allow protected properties to be referenced for retrieval, but not
248 * referenced for update.
251 public function __get( $property ) {
252 return $this->{$property};
257 * This will allow protected properties to be examined for whether they are set
258 * without making them writable. PHP 5.1 or later only.
261 public function __isset( $property ) {
262 return isset($this->{$property});
265 private function assignGuestValues() {
267 $this->exists
= false;
268 if ( empty($this->username
) ) $this->username
= translate('unauthenticated');
269 $this->fullname
= $this->displayname
= translate('Unauthenticated User');
270 $this->email
= false;
271 $this->is_principal
= true;
272 $this->is_calendar
= false;
273 $this->principal_id
= -1;
274 $this->privileges
= $this->default_privileges
= 0;
277 private function assignRowValues( $db_row ) {
278 foreach( $db_row AS $k => $v ) {
283 public function Exists() {
284 return $this->exists
;
288 public function byEmail() {
289 return $this->by_email
;
294 * Work out the username, based on elements of the path.
295 * @param string $path The path to be used.
296 * @param array $options The request options, controlling whether e-mail paths are allowed.
298 private function usernameFromPath( $path ) {
301 if ( $path == '/' ||
$path == '' ) {
302 dbg_error_log( 'Principal', 'No useful path split possible' );
303 return $session->username
;
306 $path_split = explode('/', $path );
307 @dbg_error_log
( 'Principal', 'Path split into at least /// %s /// %s /// %s', $path_split[1], $path_split[2], $path_split[3] );
309 $username = $path_split[1];
310 if ( $path_split[1] == 'principals' && isset($path_split[3]) ) {
311 $username = $path_split[3];
312 $this->original_request_url
= $path;
314 if ( substr($username,0,1) == '~' ) {
315 $username = substr($username,1);
316 $this->original_request_url
= $path;
319 if ( isset($c->allow_by_email
) && $c->allow_by_email
&& preg_match( '#^(\S+@\S+[.]\S+)$#', $username) ) {
320 // This might seem inefficient, but we cache the result, so the second time will not read from the DB
321 $p = new Principal('email',$username);
322 $username = $p->username
;
323 $this->by_email
= true;
330 * Return the username
331 * @return string The username
333 function username() {
334 return (isset($this->username
)?
$this->username
:false);
339 * Set the username - but only if the record does not yet exist!
340 * @return string The username
342 function setUsername($new_username) {
343 if ( $this->exists
&& isset($this->username
) ) return false;
344 $this->username
= $new_username;
345 return $this->username
;
351 * @return int The user_no
354 return (isset($this->user_no
)?
$this->user_no
:false);
359 * Return the principal_id
360 * @return string The principal_id
362 function principal_id() {
363 return (isset($this->principal_id
)?
$this->principal_id
:false);
369 * @return string The email
372 return (isset($this->email
)?
$this->email
:false);
377 * Return the partial path representing this principal
378 * @return string The dav_name
380 function dav_name() {
381 if ( !isset($this->dav_name
) ) {
382 if ( !isset($this->username
) ) {
383 throw new Exception('Can\'t calculate dav_name for unknown username');
385 $this->dav_name
= '/'.$this->username
.'/';
387 return $this->dav_name
;
392 * Ensure the principal's dead properties are loaded
394 protected function FetchDeadProperties() {
395 if ( isset($this->dead_properties
) ) return;
397 $this->dead_properties
= array();
398 $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name()) );
399 if ( $qry->Exec('Principal') ) {
400 while ( $property = $qry->Fetch() ) {
401 $this->dead_properties
[$property->property_name
] = DAVResource
::BuildDeadPropertyXML($property->property_name
,$property->property_value
);
408 * Fetch the list of collections for this principal
409 * @return string The internal dav_name for the home_calendar, or null if there is none
411 protected function FetchCollections() {
412 if ( isset($this->collections
) ) return;
414 $this->collections
= array();
415 $qry = new AwlQuery('SELECT * FROM collection WHERE user_no= :user_no', array(':user_no' => $this->user_no()) );
416 if ( $qry->Exec('Principal') ) {
417 while ( $collection = $qry->Fetch() ) {
418 $this->collections
[$collection->dav_name
] = $collection;
425 * Return the default calendar for this principal
426 * @return string The internal dav_name for the home_calendar, or false if there is none
428 function default_calendar() {
431 if ( !isset($this->default_calendar
) ) {
432 $this->default_calendar
= false;
433 if ( !isset($this->dead_properties
) ) $this->FetchDeadProperties();
434 if ( isset($this->dead_properties
['urn:ietf:params:xml:ns:caldav:schedule-default-calendar-URL']) ) {
435 $this->default_calendar
= $this->dead_properties
['urn:ietf:params:xml:ns:caldav:schedule-default-calendar-URL'];
438 if ( !isset($this->collections
) ) $this->FetchCollections();
439 $dav_name = $this->dav_name().$c->home_calendar_name
.'/';
440 if ( isset($this->collections
[$dav_name]) && ($this->collections
[$dav_name]->is_calendar
== 't') ) {
441 $this->default_calendar
= $dav_name;
444 $dav_name = $this->dav_name().'home/';
445 if ( isset($this->collections
[$dav_name]) && ($this->collections
[$dav_name]->is_calendar
== 't') ) {
446 $this->default_calendar
= $dav_name;
449 foreach( $this->collections
AS $dav_name => $collection ) {
450 if ( $collection->is_calendar
== 't' ) {
451 $this->default_calendar
= $dav_name;
458 return $this->default_calendar
;
463 * Return the URL for this principal
464 * @param string $type The type of URL we want (the principal, by default)
465 * @param boolean $internal Whether an internal reference is requested
466 * @return string The principal-URL
468 public function url($type = 'principal', $internal=false ) {
472 $result = $this->dav_name();
474 if ( isset($this->original_request_url
) && $type == 'principal' )
475 $result = $this->original_request_url
;
477 $result = $this->url
;
481 case 'principal': break;
482 case 'schedule-default-calendar': $result = $this->default_calendar(); break;
483 case 'schedule-inbox': $result .= '.in/'; break;
484 case 'schedule-outbox': $result .= '.out/'; break;
485 case 'dropbox': $result .= '.drop/'; break;
486 case 'notifications': $result .= '.notify/'; break;
488 fatal('Unknown internal URL type "'.$type.'"');
490 return ConstructURL(DeconstructURL($result));
494 public function internal_url($type = 'principal' ) {
495 return $this->url($type,true);
499 public function unCache() {
500 if ( !isset($this->cacheNs
) ) return;
501 $cache = new AwlCache();
502 $cache->delete($this->cacheNs
, null );
506 private function Write( $field_values, $inserting=true ) {
508 if ( is_array($field_values) ) $field_values = (object) $field_values;
510 if ( !isset($field_values->{'user_active'}) ) {
511 if ( isset($field_values->{'active'}) )
512 $field_values->{'user_active'} = $field_values->{'active'};
513 else if ( $inserting )
514 $field_values->{'user_active'} = true;
516 if ( !isset($field_values->{'modified'}) && isset($field_values->{'updated'}) )
517 $field_values->{'modified'} = $field_values->{'updated'};
518 if ( !isset($field_values->{'type_id'}) && $inserting )
519 $field_values->{'type_id'} = 1; // Default to 'person'
520 if ( !isset($field_values->{'default_privileges'}) && $inserting )
521 $field_values->{'default_privileges'} = sprintf('%024s',decbin(privilege_to_bits($c->default_privileges
)));
526 $insert_fields = array();
527 $param_names = array();
530 $update_list = array();
532 $sql_params = array();
533 foreach( self
::updateableFields() AS $k ) {
534 if ( !isset($field_values->{$k}) && !isset($this->{$k}) ) continue;
536 $param_name = ':'.$k;
537 $sql_params[$param_name] = (isset($field_values->{$k}) ?
$field_values->{$k} : $this->{$k});
538 if ( $k == 'default_privileges' ) {
539 $sql_params[$param_name] = sprintf('%024s',$sql_params[$param_name]);
540 $param_name = 'cast('.$param_name.' as text)::BIT(24)';
542 else if ( $k == 'modified'
543 && isset($field_values->{$k})
544 && preg_match('{^([23]\d\d\d[01]\d[0123]\d)T?([012]\d[0-5]\d[0-5]\d)$}', $field_values->{$k}, $matches) ) {
545 $sql_params[$param_name] = $matches[1] . 'T' . $matches[2];
549 $param_names[] = $param_name;
550 $insert_fields[] = $k;
553 $update_list[] = $k.'='.$param_name;
557 if ( $inserting && isset(self
::$db_mandatory_fields) ) {
558 foreach( self
::$db_mandatory_fields AS $k ) {
559 if ( !isset($sql_params[':'.$k]) ) {
560 throw new Exception( get_class($this).'::Create: Mandatory field "'.$k.'" is not set.');
563 if ( isset($this->user_no
) ) {
564 $param_names[] = ':user_no';
565 $insert_fields[] = 'user_no';
566 $sql_params[':user_no'] = $this->user_no
;
568 if ( isset($this->created
) ) {
569 $param_names[] = ':created';
570 $insert_fields[] = 'created';
571 $sql_params[':created'] = $this->created
;
573 $sql = 'INSERT INTO '.self
::$db_tablename.' ('.implode(',',$insert_fields).') VALUES('.implode(',',$param_names).')';
576 $sql = 'UPDATE '.self
::$db_tablename.' SET '.implode(',',$update_list);
577 $sql .= ' WHERE principal_id=:principal_id';
578 $sql_params[':principal_id'] = $this->principal_id
;
581 $qry = new AwlQuery($sql, $sql_params);
582 if ( $qry->Exec('Principal',__FILE__
,__LINE__
) ) {
584 $new_principal = new Principal('username', $sql_params[':username']);
585 foreach( $new_principal AS $k => $v ) {
592 public function Create( $field_values ) {
593 $this->Write($field_values, true);
596 public function Update( $field_values ) {
597 if ( !$this->Exists() ) {
598 throw new Exception( get_class($this).'::Create: Attempting to update non-existent record.');
600 $this->Write($field_values, false);
603 static public function cacheFlush( $where, $whereparams=array() ) {
604 $cache = new AwlCache();
605 if ( !$cache->isActive() ) return;
606 $qry = new AwlQuery('SELECT dav_name FROM dav_principal WHERE '.$where, $whereparams );
607 if ( $qry->Exec('Principal',__FILE__
,__LINE__
) ) {
608 while( $row = $qry->Fetch() ) {
609 $cache->delete('principal-'.$row->dav_name
, null);
614 static public function cacheDelete( $type, $value ) {
615 $cache = new AwlCache();
616 if ( !$cache->isActive() ) return;
617 if ( $type == 'username' ) {
618 $value = '/'.$value.'/';
620 $cache->delete('principal-'.$value, null);