source: trunk/jpdd/class.dkg.site.php @ 562

Last change on this file since 562 was 562, checked in by dkg, 6 years ago

JPDD: editing people now shows person_roles appropriately tied to the affiliated organization.

File size: 59.1 KB
Line 
1<?php  /* -*- indent-tabs-mode: nil; tab-width: 4; c-basic-offset: 2; -*-
2
3       */
4
5if (!class_exists('DKG_Site')) {
6
7  class DKG_Site {
8       
9        // expects a postgresql database on the localhost, with db
10        // authentication to be handled cleanly elsewhere:
11        var $_db_name;
12
13    var $_site_name;
14    var $_active_event;
15    var $_site_date;
16    var $_site_subtitle;
17    var $_base_path;
18    var $_hostname;
19
20    var $_db;
21
22    var $_action;
23    var $_type;
24    var $_identifier;
25    var $_extra_URI_args;
26    var $_class_map;
27
28    var $_page_title;
29    var $_icon;
30    var $_stylesheets;
31    var $_warnings;
32
33    // authentication session cookie info:
34    var $_session;
35
36    // info about the table used for user authentication:
37    var $_users;
38
39
40    // for dealing with randomly-generated strings:
41    var $_rand_chars_string;
42    var $_rand_chars_array;
43
44    var $_authenticated_user;
45
46    // in hours:
47    var $_resetpass_expiration;
48    var $_new_account_confirmation_expiration;
49
50    var $_site_email_from;
51    var $_actually_send_email;
52
53    var $_lib_locations;
54
55    function DKG_Site() {
56      $this->_icon = 'icon.png';
57      $this->_stylesheets = array('styles.css');
58      $this->_sourced_scripts = array(); // path names, relative to the site root.
59      $this->_scripts = array(); // pieces of actual javascript to execute.
60      $this->_users = array('table' => 'person',
61                            'userid' => 'id',
62                            'username' => 'email',
63                            'session' => 'session_value',
64                            'password' => 'pass',
65                            'session_touched' => 'session_touched',
66                            'session_created' => 'session_created',
67                            'resetpass' => 'resetpass',
68                            'resetpass_created' => 'resetpass_created',
69                            'cookie_name' => 'xx_username');
70      $this->_session = array('cookie_name' => 'xx_session',
71                              'timeout_minutes' => 120,
72                              'string_length' => 30);
73      $this->_warnings = array();
74
75      $this->_rand_chars_string = 'ABCDEFGHIJKLMNOPQRSTUWXYZabcdefghijklmnopqrstuwxyz0123456789';
76      $this->_rand_chars_array = str_split($this->_rand_chars_string);
77
78      $this->_authenticated_user = NULL;
79
80      require_once('config.inc.php');
81      $this->_db_name = $site_config['db_name'];
82      $this->_site_name = $site_config['site_name'];
83      $this->_active_event = $site_config['active_event'];
84      $this->_base_path = $site_config['base_path'];
85      $this->_hostname = $site_config['hostname'];
86      $this->_page_title = $this->_site_name;
87      $this->_site_date = $site_config['site_date'];
88      $this->_site_subtitle = $site_config['site_subtitle'];
89      $this->_resetpass_expiration = (array_key_exists('resetpass_expiration', $site_config) ? $site_config['resetpass_expiration'] : 2);
90      $this->_new_account_confirmation_expiration = (array_key_exists('new_account_confirmation_expiration', $site_config) ? $site_config['new_account_confirmation_expiration'] : 24);
91      $this->_site_email_from = $site_config['site_email_from'];
92      $this->_actually_send_email = (array_key_exists('actually_send_email', $site_config) ? $site_config['actually_send_email'] : true);
93      $this->_signup_closed_message = (array_key_exists('signup_closed_message', $site_config) ? $site_config['signup_closed_message'] : '');
94
95      $this->_class_map = array();
96
97      // these can be overridden by the subclass by re-adding the same addClassMapEntry.
98      $this->addClassMapEntry('person', array('classname' => 'DKG_Person', 
99                                              'filename' => 'class.dkg.person.php',
100                                              'table' => $this->_users['tablename'],
101                                              'sort' => $this->_users['username']));
102      $this->addClassMapEntry('privilege', array('classname' => 'DKG_Privilege', 
103                                              'filename' => 'class.dkg.privilege.php',
104                                              'table' => 'privilege',
105                                              'sort' => 'title'));
106
107      $this->_lib_locations = array('fpdf' => '/usr/share/php/fpdf');
108
109      $this->_db = pg_connect('dbname='.$this->_db_name) or error('Failed to connect to database!');
110      $this->_type = NULL;
111      $this->_identifier = NULL;
112      $this->_extra_URI_args = array();
113
114
115      // clean up magic_quotes stupidity:
116      $_POST = $this->unmagicquotes($_POST);
117      $_GET = $this->unmagicquotes($_GET);
118      $_COOKIE = $this->unmagicquotes($_COOKIE);
119    }
120
121    // stuff that must be done after the subclass is done
122    // initializing...
123    function initialize() {
124      $this->evaluateSession();
125      $this->interpretURI();
126    }
127
128
129    function getSiteDate() {
130      return (!$this->isEmpty($this->_site_date) ? $this->_site_date : NULL);
131    }
132    function getSiteSubtitle() {
133      return (!$this->isEmpty($this->_site_subtitle) ? $this->_site_subtitle : NULL);
134    }
135    function getSiteName() {
136      return $this->_site_name;
137    }
138    function getActiveEventID() {
139      return (int)$this->_active_event;
140    }
141
142    // we're relying on a functioning mail() function.
143    // returning true means it actually was queued for delivery.
144    // returning false means it was never queued.
145    function mail($type, $to,$subj,$body, $hdrs = array(), $personal = false, $broadcast_id = NULL) {
146      if (!is_array($hdrs))
147        $hdrs = array($hdrs);
148     
149      $hdrs[] = 'From: '.trim(str_replace("\n", '', ($personal && $this->isAuthenticated() ? $this->_authenticated_user->getTitle().' <'.$this->_authenticated_user->_email.'>' : $this->_site_name.' <'.$this->_site_email_from.'>')));
150      $hdrs = join("\r\n", $hdrs);
151
152      // log it:
153      $this->executeSQL('INSERT INTO mail_log (mail_from, mail_to, subject, body, extra_headers, message_type, actually_sent, broadcast_id) VALUES ('.$this->escStr($this->_site_email_from).','.
154                        $this->escStr($to).','.
155                        $this->escStr($subj).','.
156                        $this->escStr($body).','.
157                        $this->escStr($hdrs).','.
158                        $this->escStr($type).','.
159                        ($this->_actually_send_email ? 'true' : 'false').','.
160                        $this->intOrDefault($broadcast_id).
161                        ')', false);
162      if ($this->_actually_send_email)
163        return mail($to, $subj, $body, $hdrs);
164      else
165        return false; // we didn't actually send the mail.
166    }
167
168    function addWarning($warn) {
169      $this->_warnings[] = $warn;     
170    }
171
172    function getSingletonTable($classname) {
173      $arr = array_filter($this->_class_map, create_function('$item', 'return $item["classname"] == "'.$classname.'";'));
174      if (count($arr) != 1)
175        $this->error('Failed to getSingletonTable for class '.$classname);
176     
177      $a = array_shift($arr);
178      return $a['table'];
179    }
180
181    function getShortName($classname) {
182      $arr = array_filter($this->_class_map, create_function('$item', 'return $item["classname"] == "'.$classname.'";'));
183      if (count($arr) != 1)
184        $this->error('Failed to getShortName for class '.$classname);
185     
186      return array_shift(array_keys($arr));
187    }
188
189    function getRandString($chars) {
190      $ret = '';
191      while ($chars > 0) {
192        $ret .= $this->_rand_chars_array[array_rand($this->_rand_chars_array)];
193        $chars--;
194      }
195      return $ret;
196    }
197
198    function getAllowedActions() {
199      return array('edit', 'login', 'logout', 'resetpass', 'newacct', 'broadcast');
200    }
201   
202    function interpretURI() {
203      $stripped = preg_replace('|^'.$this->_base_path.'|', '', $_SERVER['REQUEST_URI']);
204      //      $stripped = preg_replace('|[^a-zA-Z0-9_-]|g', '', $stripped);
205      $pieces = split('/', $stripped);
206      while (is_numeric($pieces[0])) {
207        $this->_active_event = $pieces[0];
208        array_shift($pieces);
209      }
210      if (in_array($pieces[0], $this->getAllowedActions())) {
211        $this->_action = array_shift($pieces);
212      } else {
213        $this->_action = 'view';
214      }
215      if (!in_array($this->_action, array('resetpass', 'newacct')))
216        $this->_type = array_shift($pieces);
217      $this->_identifier = array_shift($pieces);
218      $this->_extra_URI_args = $pieces;
219    }
220
221    // sets up _authenticated_user based on the contents of the cookies.
222    function evaluateSession() {
223      if (array_key_exists($this->_session['cookie_name'], $_COOKIE) &&
224          array_key_exists($this->_users['cookie_name'], $_COOKIE)) {
225        $authenticatedUserID = $this->checkSession($_COOKIE[$this->_users['cookie_name']],
226                                                   $_COOKIE[$this->_session['cookie_name']]);
227        if (!is_null($authenticatedUserID)) {
228          $map = $this->prepClass('person');
229          $cname = $map['classname'];
230          $this->_authenticated_user = new $cname(array('id' => $authenticatedUserID));
231          $this->freshenSession($this->getAuthenticatedUserID());
232          // don't allow caching of authenticated pages.
233          header("Pragma: no-cache");
234          header("Cache-control: no-cache");
235          header("Expires: Fri, 01 Jan 1999 00:00:00 GMT");
236        }
237      }
238    }
239
240    function isAuthenticated() {
241      return !(is_null($this->_authenticated_user));
242    }
243   
244    function getAuthenticatedUserID() {
245      if (!$this->isAuthenticated())
246        return NULL;
247      return $this->_authenticated_user->getID();
248    }
249   
250    // this should be overridden by the instantiating class.
251    function getDefaultPage() {
252      return '<h1>'.$this->_site_name.'</h1>';
253    }
254
255    // returns a personID if this is a successful login with the
256    // user's password.  returns NULL otherwise.
257
258    // users with a NULL password (or non-existent users) will always
259    // return false.
260    function checkPassword($username, $password) {
261      $xx = $this->getSeriesFromSQL('SELECT '.$this->_users['userid'].' AS userid, '.$this->_users['password'].' AS password FROM '.
262                                    $this->_users['table'].' WHERE '.
263                                    $this->_users['username'].' = '.$this->escStr($username));
264      if (count($xx) != 1) {
265        // not a valid match!
266        return NULL;
267      } else {
268        $user = array_shift($xx);
269        if ($user['password'] == sha1($user['userid'].'!'.$password)) 
270          return (int)($user['userid']);
271        else
272          return NULL;
273      }
274    }
275
276    // same thing as checkPassword, but only looking at session IDs:
277    function checkSession($username, $session) {
278      $xx = $this->getValuesFromSQL('SELECT '.$this->_users['userid'].' AS userid FROM '.$this->_users['table'].' WHERE '.
279                                    $this->_users['username'].' = '.$this->escStr($username).' AND '.
280                                    $this->_users['session'].' = '.$this->escStr($session).' AND '.
281                                    $this->_users['session_touched'].' > CURRENT_TIMESTAMP - INTERVAL '.
282                                    $this->escStr((int)$this->_session['timeout_minutes'].' MINUTES'),
283                                    'userid');
284      if (count($xx) != 1) {
285        return NULL;
286      } else {
287        return array_shift($xx);
288      }
289    }
290
291
292    // touch the session to keep it fresh:
293    function freshenSession($uid) {
294      $xx = $this->executeSQL('UPDATE '.$this->_users['table'].' SET '.$this->_users['session_touched'].' = CURRENT_TIMESTAMP WHERE '.$this->_users['userid'].' = '.(int)$uid);
295    }
296
297
298    // ensures that there is a valid session for the given user id,
299    // and returns it.
300    function getValidSessionForUserID($uid, $forcenewsession = false) {
301      $validsession = array();
302      if (!$forcenewsession) 
303        $validsession = $this->getValuesFromSQL('SELECT '.$this->_users['session'].' AS session FROM '.$this->_users['table'].' WHERE '.
304                                                $this->_users['userid'].' = '.(int)$uid.' AND '.
305                                                $this->_users['session'].' IS NOT NULL AND '.
306                                                $this->_users['session_touched'].' > CURRENT_TIMESTAMP - INTERVAL '.
307                                                $this->escStr((int)$this->_session['timeout_minutes'].' MINUTES'), 
308                                                'session');
309      if (count($validsession) == 1) {
310        $validsession = array_shift($validsession);
311      } else {
312        // make a new session, and store it in the table and in $validsession
313        $validsession = $this->getRandString($this->_session['string_length']);
314        $this->executeSQL('UPDATE '.$this->_users['table'].' SET '.$this->_users['session'].' = '.$this->escStr($validsession).', '.
315                          $this->_users['session_created'].' = CURRENT_TIMESTAMP, '.
316                          $this->_users['session_touched'].' = CURRENT_TIMESTAMP WHERE '.
317                          $this->_users['userid'].' = '.(int)$uid);
318      } 
319      return $validsession;
320    }
321
322    // the form token should match the existing session cookie.
323    function getFormTokenHiddenInput() {
324      if (array_key_exists($this->_session['cookie_name'], $_COOKIE) &&
325          preg_match(':^['.$this->_rand_chars_string.']{'.$this->_session['string_length'].'}$:', $_COOKIE[$this->_session['cookie_name']])) {
326        return '<input type="hidden" name="form_token" value="'.$_COOKIE[$this->_session['cookie_name']].'"/>';
327      } else {
328        // the cookie either doesn't exist, or doesn't match what we
329        // expect a session ID to look like.  no form token for you!
330        return '';
331      }
332    }
333
334    function verifyFormToken() {
335      return $_COOKIE[$this->_session['cookie_name']] === $_POST['form_token'];
336    }
337
338    // FIXME: $prefix is being applied directly within a preg match,
339    // without testing.  watch out for funny chars in yer prefix.
340    // they won't work right.
341    function getPostedIDtoIDMap($prefix) {
342      $ret = array();
343      reset($_POST);
344      while(list($k,$v) = each($_POST)) {
345        if (preg_match('/^'.$prefix.'_([[:digit:]]+)$/', $k, $m))
346          $ret[(int)($m[1])] = ($this->isEmpty($v) ? NULL : (int)$v);
347      }
348      return $ret;
349    }
350
351    // same as the above function, but doesn't limit the values to
352    // numeric IDs or NULL.  values can be anything.
353    function getPostedIDMap($prefix) {
354      $ret = array();
355      reset($_POST);
356      while(list($k,$v) = each($_POST)) {
357        if (preg_match('/^'.$prefix.'_([[:digit:]]+)$/', $k, $m))
358          $ret[(int)($m[1])] = $v;
359      }
360      return $ret;
361    }
362
363    function getPostedIDs($varname) {
364      if (!array_key_exists($varname, $_POST))
365        return array();
366      $x = $_POST[$varname];
367      if (!is_array($x)) {
368        if ($this->isEmpty($x))
369          return array();
370        return array((int)$x);
371      }
372      return array_map('intval', array_filter($x, create_function('$t','global $dkg_site; return !$dkg_site->isEmpty($t);')));       
373    }
374
375    function getWarning($str) {
376      return (is_null($str) || '' === $str) ? '' : '<div class="warning">'.$str.'</div>';
377    }
378
379    function handleLogout($followupURI) {
380      if ($this->isAuthenticated()) {
381        // verify the form token
382        if (!$this->verifyFormToken()) {
383          return $this->getWarning('Something is drastically wrong with your form token');
384        } 
385        // drop the session from the table:
386        $this->executeSQL('UPDATE '.$this->_users['table'].' SET '.$this->_users['session'].' = NULL, '.
387                          $this->_users['session_touched'].' = NULL WHERE '.
388                          $this->_users['userid'].' = '.(int)$this->getAuthenticatedUserID());
389       
390        // clear the cookies (see handleLogin() about why i'm omitting the hostname)
391        setcookie($this->_session['cookie_name'], '', 0, $this->_base_path);//, $this->_hostname);
392        setcookie($this->_users['cookie_name'], '', 0, $this->_base_path);//, $this->_hostname);
393      }
394      // FIXME: should use redirectLocal()
395      // redirect to the followupURI.
396      header('Location: http://'.$this->_hostname.$this->ensureValidURI($followupURI));
397      header('Connection: close');
398      exit;
399    }
400
401    function getAdditionalSalutation() {
402      return '';
403    }
404
405    function getLogoutForm($followupURI = NULL) {
406      return '<form id="logout_form" class="logout" action="'.$this->Path("logout").'" method="post">
407'.($this->isAuthenticated() ? '<div class="salutation">Welcome,<br/>'.$this->_authenticated_user->getLinkedTitle().$this->getAdditionalSalutation().'</div>':'').'
408<input class="submit" type="submit" value="Logout"/>
409'.$this->getFormTokenHiddenInput().'
410<input type="hidden" name="logout_followup" value="'.(is_null($followupURI) ? (in_array($this->_action, array('logout', 'login')) ? '' : $_SERVER['REQUEST_URI'] ): $followupURI).'"/>
411</form>';
412    }
413
414    function getLoginForm($followupURI = NULL, $warning = NULL) {
415      if ($this->isAuthenticated()) {
416        return $this->getLogoutForm($followupURI);
417      }
418      if (!in_array($this->_action, array('login')) && is_null($warning)) {
419        $this->addSourcedScript('scripts/dkg.base.js');
420        $this->addScriptChunk('DKG.onLoadScripts.push(\'DKG.FieldsetCollapse("login_legend", true, "'.$this->Path('login').'");\');');
421      }
422      return '<form id="login_form" class="login" action="'.$this->Path("login").'" method="post">
423<fieldset><legend id="login_legend">Login</legend>
424'.$this->getWarning($warning).'
425<label>E-mail:<br/><input type="text" id="login_username" name="login_username"/></label><br/>
426<label>Password:<br/><input type="password" id="login_password" name="login_password"/></label><br/>
427<a style="font-size: smaller;" href="'.$this->Path('resetpass').'">Forgot Password?</a>
428<div class="containsubmit"><input class="submit" type="submit" value="Login"/></div>
429<input type="hidden" name="login_followup" value="'.(is_null($followupURI) ? (in_array($this->_action, array('logout', 'login')) ? '' : $_SERVER['REQUEST_URI'] ): $followupURI).'"/>
430</fieldset>
431</form>
432';
433    }
434
435    function getResetPassRequestForm($warning = NULL) {
436      return '<form id="resetpass_form" action="'.$this->Path('resetpass').'" method="post">
437<fieldset><legend>Reset Password</legend>
438'.$this->getWarning($warning).'
439<label>E-mail:<br/><input type="text" name="reset_email"/></label>
440<div class="containsubmit"><input class="submit" type="submit" value="Reset Password"/></div>
441</fieldset>
442</form>
443';
444    }
445
446    function getNewAccountRequestForm($warning = NULL) {
447      return '<form id="newaccount_form" action="'.$this->Path('newacct').'" method="post">
448<fieldset><legend>Create New Account</legend>
449'.$this->getWarning($warning).'
450<label>E-mail:<br/><input type="text" name="reset_email"/></label>
451<div class="containsubmit"><input class="submit" type="submit" value="Signup!"/></div>
452</fieldset>
453</form>
454';
455    }
456   
457    // override to allow entry of new fields:
458    function getNewAccountFormExtraFields() {
459      return '';
460    }
461    // return an array of update statements:
462    function handleNewAccountFormExtraFields() {
463      return array();
464    }
465    // return true if extra fields are all properly validated and can
466    // continue:
467    function validateNewAccountFormExtraFields() {
468      return true;
469    }
470    // and handle any additional updates once a successful new account has been verified:
471    function handleAdditionalNewAccountUpdates() {
472      return;
473    }
474
475    function getRequiredFieldLegend() {
476      return '<div><span class="required">*</span> indicates a required field.</div>';
477    }
478    function getRequiredFieldLabel() {
479      return '<span class="required">* </span>';
480    }
481   
482
483    function getNewAccountVerifyForm($id, $token, $warning = NULL) {
484      $vals = $this->getNewAccountValues(NULL, $id);
485      if (is_null($vals) || !is_null($vals['password'])) {
486        $this->addWarning('No such new user account needs verification.');
487        return '';
488      }
489      // don't allow new account if expired or token mismatch.
490      if ($vals['resetpass'] != $token) {
491        $this->log('getNewAccountVerifyForm() token did not match ('.$token.' != '.$vals['resetpass'].')');
492        $this->addWarning('Token did not match.');
493        return '';
494      } elseif (!$vals['valid']) {
495        $this->log('getNewAccountVerifyForm() token was expired.');
496        $this->addWarning('The link you clicked has expired.  Please <a href="'.$this->Path('newacct').'">request a new link</a>.');
497        return '';
498      }
499      return '<form id="newacct_form" action="'.$this->Path('newacct', (int)$id).'" method="post">
500<input type="hidden" name="newacct_token" value="'.htmlentities($token).'" />
501<fieldset><legend>Set up new account</legend>
502<div style="font-weight:bold">'.htmlentities($vals['email']).'</div><br/>
503'.$this->getWarning($warning).$this->getRequiredFieldLegend().'
504'.$this->getNewAccountFormExtraFields().'
505<label>'.$this->getRequiredFieldLabel().'Choose Password:<br/><input type="password" name="newacct_pass_0"/></label><br/>
506<label>'.$this->getRequiredFieldLabel().'Password Again:<br/><input type="password" name="newacct_pass_1"/></label>
507<div class="containsubmit"><input class="submit" type="submit" value="Set Up Account"/></div>
508</fieldset>
509</form>
510';
511    }
512
513    function getResetPassForm($id, $token, $warning = NULL) {
514      $vals = $this->getPasswordResetValues(NULL, $id);
515      if (is_null($vals)) {
516        $this->addWarning('No such user needs password reset.');
517        return '';
518      }
519      if ((!$vals['valid']) || ($vals['resetpass'] != $token)) {
520        $this->log('getResetPassForm() token did not match ('.$token.' != '.$vals['resetpass'].')');
521        $this->addWarning('Token did not match.');
522        return '';
523      }
524      return '<form id="resetpass_form" action="'.$this->Path('resetpass', (int)$id).'" method="post">
525<fieldset><legend>Reset Password for '.htmlentities($vals['email']).'</legend>
526'.$this->getWarning($warning).'
527<label>New Password:<br/><input type="password" name="reset_pass_0"/></label><br/>
528<label>New Password Again:<br/><input type="password" name="reset_pass_1"/></label>
529<div class="containsubmit"><input class="submit" type="submit" value="Set Password"/></div>
530<input type="hidden" name="reset_pass_token" value="'.htmlentities($token).'" />
531</fieldset>
532</form>
533';
534    }
535   
536    // pass it either an email or an id:
537    function getPasswordResetValues($email, $id = NULL) {
538      $sql = 'SELECT id, email, '.$this->_users['resetpass'].' AS resetpass, '.
539        '('.$this->_users['resetpass_created'].' + '.$this->escStr($this->_resetpass_expiration.' HOURS').' > NOW()) AS valid '.
540        'FROM '.$this->_users['table'].' WHERE '.$this->_users['password'].' IS NOT NULL AND '.
541        (is_null($email) ? 'id = '.(int)$id : $this->_users['username'].' = '.$this->escStr($email));
542      $vals = $this->getSingletonFromSQL($sql, false);
543      if (!is_null($vals)) 
544        // clean up $vals['valid']:
545        $vals['valid'] = ($vals['valid'] === 't');
546      return $vals;
547    }
548    function getNewAccountValues($email, $id = NULL) {
549      $sql = 'SELECT id, email, '.$this->_users['resetpass'].' AS resetpass, '.
550        $this->_users['password'].' AS password, '.
551        '('.$this->_users['resetpass_created'].' + '.$this->escStr($this->_new_account_confirmation_expiration.' HOURS').' > NOW()) AS valid '.
552        'FROM '.$this->_users['table'].' WHERE '.
553        (is_null($email) ? 'id = '.(int)$id : $this->_users['username'].' = '.$this->escStr($email));
554      $vals = $this->getSingletonFromSQL($sql, false);
555      if (!is_null($vals)) 
556        // clean up $vals['valid']:
557        $vals['valid'] = ($vals['valid'] === 't');
558      return $vals;
559    }
560
561    function handleResetPass($id, $token, $pass0, $pass1) {
562      $vals = $this->getPasswordResetValues(NULL, $id);
563      if (is_null($vals)) {
564        $this->addWarning('No such user needs password reset.');
565        return '';
566      }
567      if ((!$vals['valid']) || ($vals['resetpass'] != $token)) {
568        $this->log('handleResetPass() token did not match ('.$token.' != '.$vals['resetpass'].')');
569        $this->addWarning('Token did not match.');
570        return '';
571      }
572      if ($pass0 != $pass1) {
573        return $this->getResetPassForm($id, $token, 'New passwords did not match.  Please make sure you enter them identically.');
574      }
575      // FIXME: do a reasonable strength test for the passwords.
576      if ($pass0 == '') {
577        return $this->getResetPassForm($id, $token, 'Please choose a password.');
578      }
579      $sql = 'UPDATE '.$this->_users['table'].' SET '.
580        $this->_users['resetpass'].' = NULL, '.
581        $this->_users['resetpass_created'].' = NULL, '.
582        $this->_users['password'].' = '.$this->escStr(sha1($vals['id'].'!'.$pass0)).
583        'WHERE id = '.(int)$id;
584      $this->executeSQL($sql);
585     
586      // this should log the user in and return them to the main page.
587      return $this->handleLogin($vals['email'], $pass0, '');
588    }
589
590    function handleResetPassRequest($email) {
591      // check for the presence of such an e-mail:
592      $vals = $this->getPasswordResetValues($email);
593      if (is_null($vals)) {
594        $this->addWarning('Reset Password failed!');
595        return $this->getResetPassRequestForm('<q>'.htmlentities($email).'</q> is not a registered e-mail address!');
596      } else {
597        // we've got a person ID now. 
598        // no matter what, bump resetpass_created time.
599        $updates = array(  $this->_users['resetpass_created'].' = NOW()');
600
601        if (!$vals['valid']) {
602          //If valid is not true, create a resetpass value, and store it:
603          $vals['resetpass'] = $this->getRandString(30);
604          $updates[] = $this->_users['resetpass'].' = '.$this->escStr($vals['resetpass']);
605        }
606        $this->executeSQL('UPDATE '.$this->_users['table'].' SET '.
607                          join(', ', $updates).
608                          'WHERE id = '.(int)$vals['id']);
609        $url = 'http://'.$this->_hostname.$this->Path('resetpass', (int)$vals['id'], $vals['resetpass']);
610        $msg = 'A password reset has been requested for your account at '.$this->_site_name.'.
611You can followup on the password change by clicking the link below:
612
613  '.$url.'
614
615If you did not request this change, please ignore this message.';
616        $this->mail('passwdreset', $vals['email'],'Password Reset Requested for '.$this->_site_name,$msg);
617        return 'Please check your e-mail box for instructions to follow up on the password reset.';
618      }
619    }
620    function handleNewAccountRequest($email) {
621      // If it's not a valid e-mail, reject out of hand:
622      $errs = $this->isValid('email', $email);
623      if (!is_null($errs)) {
624        return $this->getNewAccountRequestForm('<q>'.htmlentities($email).'</q>: '.$errs);
625      }
626
627      // check for the presence of such an e-mail:
628      $vals = $this->getNewAccountValues($email);
629      if ((!is_null($vals)) && !is_null($vals['password'])) {
630        $this->addWarning('Account signup failed!');
631        return $this->getNewAccountRequestForm('<q>'.htmlentities($email).'</q> is already a registered e-mail address!');
632      } 
633      if (is_null($vals)) {
634        // insert the new e-mail address:
635        $sql = 'INSERT INTO '.$this->_users['table'].' ('.$this->_users['username'].') VALUES '.
636          '('.$this->escStr($email).')';
637        $this->executeSQL($sql);
638        $vals = $this->getNewAccountValues($email);
639      }
640      $updates = array($this->_users['resetpass_created'].' = NOW()');
641      if (!$vals['valid']) {
642        //If valid is not true, create a resetpass value, and store it:
643        $vals['resetpass'] = $this->getRandString(30);
644        $updates[] = $this->_users['resetpass'].' = '.$this->escStr($vals['resetpass']);
645      }
646      $this->executeSQL('UPDATE '.$this->_users['table'].' SET '.
647                        join(', ', $updates).
648                        'WHERE id = '.(int)$vals['id']);
649      $url = 'http://'.$this->_hostname.$this->Path('newacct', (int)$vals['id'], $vals['resetpass']);
650      $msg = 'You have requested an account at '.$this->_site_name.'.
651You can finish setting up your account by clicking the link below:
652
653  '.$url.'
654
655If you did not request this account, you can safely ignore this message.';
656      $this->mail('newacct', $vals['email'],'New Account Requested for '.$this->_site_name,$msg);
657      return 'Please check your e-mail for instructions to finish setting up your new account.';
658    }
659
660    function handleNewAccountVerify($id, $token, $pass0, $pass1) {
661      $vals = $this->getNewAccountValues(NULL, (int)$id);
662      if (is_null($vals) || !is_null($vals['password'])) {
663        $logid = $this->log('new account verify '.(int)$id.' failed because either account is missing, or already has password.');
664        $this->addWarning('New Account Verification Failed! (error: '.(int)$logid.')');
665        return $this->getDefaultPage();
666      } 
667      if ($vals['resetpass'] != $token) {
668        $logid = $this->log('new account verify tokens did not match ('.$token.' != '.$vals['resetpass'].').');
669        return $this->addWarning('Tokens did not match! (error: '.(int)$logid.')');
670      }
671      if ($pass0 != $pass1) {
672        return $this->getNewAccountVerifyForm($vals['id'], $token, 'Passwords did not match!  Please enter your new password identically.');
673      }
674      if ($pass0 == '') {
675        return $this->getNewAccountVerifyForm($vals['id'], $token, 'Please choose a new password and enter it in both fields!');
676      }
677      if (!$this->validateNewAccountFormExtraFields()) {
678        return $this->getNewAccountVerifyForm($vals['id'], $token, 'Your account setup is not complete.  Please fix the errors below first!');
679      }
680      // if we get here, we've got: valid account, valid token, valid, duplicated passwords.
681
682      // And deal with other user information here:
683      $newfields = $this->handleNewAccountFormExtraFields();
684      $newfields[] = $this->_users['resetpass'].' = NULL';
685      $newfields[] = $this->_users['resetpass_created'].' = NULL';
686      $newfields[] = $this->_users['password'].' = '.$this->escStr(sha1($vals['id'].'!'.$pass0));
687
688      $sql = 'UPDATE '.$this->_users['table'].' SET '.
689        join(', ', $newfields).
690        'WHERE id = '.(int)$id;
691      $this->executeSQL($sql);
692
693      $this->handleAdditionalNewAccountUpdates((int)$id);
694      return $this->handleLogin($vals['email'], $pass0, '');
695    }
696
697    function redirectLocal($URI) {
698      header("Location: http://".$this->_hostname.$this->ensureValidURI($URI));
699     
700      // try to force this view to not be cached:
701      header("Pragma: no-cache");
702      header("Cache-control: no-cache");
703      header("Expires: Fri, 01 Jan 1999 00:00:00 GMT");
704      // and force-close the connection after this redirect, because
705      // we want to make sure that the next load happens *with* our
706      // new cookies.
707      header("Connection: close");
708
709      //often, one would exit after this.
710    }
711
712    // FIXME: should use redirectLocal();
713    function handleLogin($username, $password, $followupURI) {
714      // Check the passphrase against the 'person' table. 
715      $uid = $this->checkPassword($username, $password);
716      if (!is_null($uid)) {
717        // if valid, verify that the session is good, forcing creation
718        // of a new session, so that two sessions triggered by a login
719        // can't happen at once.
720        $session = $this->getValidSessionForUserID($uid, true);
721
722        // and redirect the user to the followup page.
723        header("Location: http://".$this->_hostname.$this->ensureValidURI($followupURI));
724       
725        // try to force this view to not be cached:
726        header("Pragma: no-cache");
727        header("Cache-control: no-cache");
728        header("Expires: Fri, 01 Jan 1999 00:00:00 GMT");
729
730        // when i was supplying the hostname on my testbed ("squeak"),
731        // the cookies wouldn't get set.  This is possibly because the
732        // browser doesn't like to store single-level "domains", out
733        // of fear that people will set (e.g.) .com-wide cookies.
734
735        // leaving out the domain choice explicitly appears to be the
736        // best bet.
737       
738        // i'm not doing client-side timeouts because i'm handling all
739        // that on the server side, and i prefer session termination
740        // at browser close.
741        setcookie($this->_session['cookie_name'], $session, NULL, $this->_base_path);//, $this->_hostname);
742        setcookie($this->_users['cookie_name'], $username, NULL, $this->_base_path);//, $this->_hostname);
743
744        // and force-close the connection after this redirect, because
745        // we want to make sure that the next load happens *with* our
746        // new cookies.
747        header("Connection: close");
748        exit;
749      } else {
750        return $this->getLoginForm($followupURI, 'Login failed.');
751      }
752    }
753
754
755    // pass this the URL-visible class identifier.  It will preload
756    // the required files for the class, and return an array with info
757    // about the class.  If this identifier isn't present, it will
758    // trigger an error.
759    function prepClass($classshort) {
760      if (array_key_exists($classshort, $this->_class_map)) {
761        $mapvers = $this->_class_map[$classshort];
762        require_once($mapvers['filename']);
763        return $mapvers;
764      } else {
765        $this->error('Failed to prepare class '.$classshort);
766      }
767    }
768   
769    function addClassMapEntry($shortname, $entry) {
770      $this->_class_map[$shortname] = $entry;
771    }
772
773    function viewListing($type) {
774      if (!$this->testAction('List', $type)) {
775        $this->permissionDenied('You are not allowed to list objects of type '.$type);
776      } else {
777        $ret = '';
778        $items = $this->getAll($type);
779       
780        if ($this->canCreate($type)) 
781          $ret .= "\n".'<div><a href="'.$this->Path('edit', $type).'">create new '.$type.'</a></div>'."\n";
782       
783        if (count($items)) {
784          $map = $this->prepClass($type);
785          $cname = $map['classname'];
786          $ret .= call_user_func(array($cname, 'showList'), $items);
787        } else {
788          // FIXME: pluralize properly!
789          $ret .= 'There are currently no '.$type.'.';
790        }
791        return $ret;
792      }
793    }
794
795    function getCreatePrivilege($type) {
796      $map = $this->prepClass($type);
797      return call_user_func(array($map['classname'], 'getCreatePrivilege'));
798    }
799
800    function getEditPrivilege($type) {
801      $map = $this->prepClass($type);
802      return call_user_func(array($map['classname'], 'getCreatePrivilege'));
803    }
804   
805    function permissionDenied($msg) {
806      header("HTTP/1.0 403 Forbidden");
807      $this->log('Permission Denied: '.$msg);
808      print '<html><head></head><body><h1>Permission Denied</h1>'.$msg.'</body></html>';
809      exit;
810    }
811
812    function canCreate($type) {
813      return $this->testAction('Create', $type);
814    } 
815
816    function testAction($action, $type) {
817      $fname = 'get'.$action.'Privilege';
818      $map = $this->prepClass($type);
819      $p = call_user_func(array($map['classname'], $fname));
820      if (is_null($p))
821        return true;
822      elseif (!$this->isAuthenticated()) 
823        return false;
824      else
825        return $this->_authenticated_user->hasAnyOfThesePrivileges($p);
826    }
827
828    // returns NULL if OK, otherwise returns an error message
829    // explaining why it doesn't match.
830
831    // blatantly stole these regexps from lightningbug
832    function isValid($type, $string) {
833      if ($type == 'URL') {
834        if (preg_match('/^https?:\/\/[-0-9a-zA-Z]+\.[-0-9a-zA-Z.]+(\/[^\s]*)?$/', $string))
835          return NULL;
836        else
837          return 'Not a valid URL';
838      } elseif (preg_match('/^[Ee]-?mail$/', $type)) {
839        if (preg_match('/^[^@\s]+\@(\[?)([-\w]+\.)+([a-zA-Z]{2,6}|[0-9]{1,3})(\]?)$/', $string))
840          return NULL;
841        else
842          return 'Not a valid e-mail address';
843      }
844    }
845
846    function handleCreation($type) {
847      $map = $this->prepClass($type);
848      $dummy = new $map['classname'](array());
849      $error = $dummy->handleCreation();
850      if (is_null($error)) {
851        // success!  We should actually do a 302 redirect to the URL
852        // of the new object.
853        header('Location: '.$dummy->getViewPath());
854        exit;
855      } else {
856        $this->addWarning($error);
857        return $dummy->getCreationForm();
858      }
859    }
860
861    function handleEdit($item) {
862      $error = $item->handleEdit();
863      if (is_null($error)) {
864        // success!  We should actually do a 302 redirect to the URL
865        // of the new object.
866        header('Location: '.$item->getViewPath());
867        exit;
868      } else {
869        $this->addWarning($error);
870        return $item->getEditForm();
871      }
872    }
873
874    function getCreationForm($type) {
875      $map = $this->prepClass($type);
876      $dummy = new $map['classname'](array());
877      return $dummy->getCreationForm();
878    }
879
880    function getItem($type, $id) {
881      $map = $this->prepClass($type);
882
883      $cname = $map['classname'];
884      return new $cname(array('id' => $id));
885    }
886
887    // cleans up the given URI and makes sure that it is a valid
888    // dkg_site URI (consisting of $_base_path, followed by letters,
889    // numbers, hyphens, underscores, and slashes)
890    function ensureValidURI($uri) {
891      if (!preg_match(':^'.$this->_base_path.'[/a-z0-9_-]*$:', $uri)) {
892        // if it isn't legal, go back to the base path:
893        return $this->_base_path;
894      } else {
895        return $uri;
896      }
897    }
898
899    function isEmpty($x) {
900      return (is_null($x) or ('' === $x));
901    }
902    function arrayKeyIsNotEmpty($keyname, $arr) {
903      return !  ((!array_key_exists($keyname, $arr)) or is_null($arr[$keyname]) or ('' === $arr[$keyname]));
904    }
905   
906    // override this if you want to add trimmings:
907    function getPageBody() {
908      return '<div class="maincontent">'.$this->getMainContent().'</div>';
909    }
910
911    function getMainContent() {
912      if ($this->_action == 'view') {
913        if ($this->isEmpty($this->_type)) {
914          return $this->getDefaultPage();
915        } else {
916          if ($this->isEmpty($this->_identifier)) {
917            return $this->viewListing($this->_type);
918          } else {
919            $item = $this->getItem($this->_type, $this->_identifier);
920            return $item->getDetailView();
921          }
922        } 
923      } elseif ($this->_action == 'login') {
924        // if we're POSTing, this was a login attempt.
925        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
926          return $this->handleLogin($_POST['login_username'], $_POST['login_password'], $_POST['login_followup']);
927        } else {
928          // FIXME: if we're actually already logged in, we should
929          // just show the default page.
930          return $this->getLoginForm();
931        }
932      } elseif ($this->_action == 'resetpass') {
933        // FIXME: if we're already authenticated, this should take us
934        // directly to the "changepassword" form.
935        if (is_null($this->_identifier)) {
936          // this is just requesting a password reset:
937          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
938            return $this->handleResetPassRequest($_POST['reset_email']);
939          } else {
940            return $this->getResetPassRequestForm();
941          }
942        } else {
943          // this is processing a password reset:
944          // everything after the last slash is the resetpass value.
945          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
946            return $this->handleResetPass((int)$this->_identifier, $_POST['reset_pass_token'], $_POST['reset_pass_0'], $_POST['reset_pass_1']);
947          } else {
948            // grab the token from the URI:
949            return $this->getResetPassForm((int)$this->_identifier, array_shift($this->_extra_URI_args));
950          }
951        }
952      } elseif ($this->_action == 'newacct') {
953        // If we're already authenticated, this should not work at all:
954        if ($this->isAuthenticated()) {
955          $this->addWarning('You already have an account!');
956          return $this->getDefaultPage();
957        }
958        if (is_null($this->_identifier)) {
959          // this is just requesting a new account:
960          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
961            return $this->handleNewAccountRequest($_POST['reset_email']);
962          } else {
963            return $this->getNewAccountRequestForm();
964          }
965        } else {
966          // this is processing a new account e-mail verification:
967          // everything after the last slash is the verification value.
968          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
969            return $this->handleNewAccountVerify((int)$this->_identifier, $_POST['newacct_token'], $_POST['newacct_pass_0'], $_POST['newacct_pass_1']);
970          } else {
971            // grab the token from the URI:
972            return $this->getNewAccountVerifyForm((int)$this->_identifier, array_shift($this->_extra_URI_args));
973          }
974        }
975      } elseif ($this->_action == 'logout') {
976        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
977          return $this->handleLogout($_POST['logout_followup']);
978        } else {
979          return $this->getLoginForm(); // login form shows logout form if currently logged in.
980        }
981      } elseif ($this->_action == 'edit') {
982        if ($this->isEmpty($this->_type)) {
983          $this->error('Nothing to edit!');
984        }
985        if ($this->isEmpty($this->_identifier)) {
986          // we're creating a new one.  Does the user have privileges?
987          if ($this->canCreate($this->_type)) {
988            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
989              return $this->handleCreation($this->_type);
990            } else {
991              return $this->getCreationForm($this->_type);
992            }
993          } else {
994            return $this->permissionDenied('Creating '.$type.' not allowed.');
995          }
996        } else {
997          $item = $this->getItem($this->_type, $this->_identifier);
998          if (is_null($item->getEditPrivilege()) || ($this->isAuthenticated() && $this->_authenticated_user->canEdit($item))) {
999            // we're modifying an existing one:
1000            if ($_SERVER['REQUEST_METHOD'] == 'POST') {
1001              return $this->handleEdit($item);
1002            } else {
1003              return $item->getEditForm();
1004            }
1005          } else {
1006            return $this->permissionDenied('Editing '.$type.' not allowed.');
1007          }
1008        }
1009      } elseif ($this->_action == 'broadcast') {
1010        if ($this->isAuthenticated() && $this->_authenticated_user->HasAllOfThesePrivileges('Send Broadcast')) {
1011          if ($_SERVER['REQUEST_METHOD'] == 'POST') {
1012            // verify form token, or just quit:
1013            if (!$this->verifyFormToken()) {
1014              $this->addWarning('Bad form token mismatch!');
1015              return '';
1016            }
1017            if ($_POST['submit'] == 'Send') {
1018              return $this->sendBroadcastEmails();
1019            } else {
1020              return $this->getBroadcastEmailForm();
1021            }
1022          } else {
1023            return $this->getBroadcastEmailForm();
1024          }
1025        } else
1026          return $this->permissionDenied('Broadcast not allowed.');
1027      } else {
1028        return $this->getMainContentExtended();
1029      }
1030    }
1031
1032    function getBroadcastSelectors() {
1033      // override to return an array that maps internal keys to
1034      // visible labels and SQL statements.  For example:
1035      return array('all' => array('description' => 'Every account with a confirmed e-mail',
1036                                  'sql' => 'SELECT * FROM person WHERE pass IS NOT NULL'));
1037    }
1038
1039    function getAllowedTemplates() {
1040      // override to return an array that maps selectors to
1041      // array('description' = "human readable", 'function' => actual
1042      // php function)
1043      return array('[[NAME]]' => array('description' => 'The person\'s name',
1044                                       'function' => create_function('$x', 'return $x->getTitle();')));
1045    }
1046   
1047    function getAllowedTemplateLegend() {
1048      $temps = $this->getAllowedTemplates();
1049      return "<div class=\"template-legend\">Allowed personalization terms:<dl>\n".
1050        join('', array_map(create_function('$x,$y', 'return "<dt>".$x."</dt>\n<dd>".$y["description"]."</dd>\n";'),
1051                           array_keys($temps), $temps))."</dl>\n</div>\n";
1052    }
1053   
1054    function getBroadcastSelectorInput($selector_name = NULL) {
1055      $selectors = $this->getBroadcastSelectors();
1056      return '<select name="selector">
1057'.join("\n", array_map(create_function('$v, $l', 'global $dkg_site; return "<option value=\"".$v."\" ".($v == "'.$selector_name.'" ? "selected" : "").">".$l["description"]." (".$dkg_site->getValueFromSQL("SELECT COUNT(*) AS count FROM (".$l["sql"].") AS selector", "count").")</option>";'), array_keys($selectors), $selectors)).'
1058</select>
1059';
1060    }
1061   
1062    // $txt can be either a string or an array of strings.  the return
1063    // value will match.
1064    function applyBroadcastTemplate($txt, $user) {
1065      $temps = $this->getAllowedTemplates();
1066      $vals = array();
1067      reset($temps);
1068      while(list($temp,$x) = each($temps)) {
1069        $vals[] = $x['function']($user);
1070      }
1071      return str_ireplace(array_keys($temps), $vals, $txt);
1072    }
1073
1074    function sendBroadcastEmails() {
1075      if ($_SERVER['REQUEST_METHOD'] != 'POST') {
1076        $this->addWarning('You cannot send broadcasts with a GET request.');
1077        return '';
1078      }
1079
1080      $subj = $_POST['subject'];
1081      $content = $_POST['content'];
1082      $personal = ($_POST['personal'] == 'personal');
1083     
1084      $selector_name = $_POST['selector'];
1085      $selectors = $this->getBroadcastSelectors();
1086      if (!array_key_exists($selector_name, $selectors)) {
1087        $this->addWarning('You chose an unavailable selector!');
1088        return '';
1089      }
1090      $selector = $selectors[$selector_name];
1091
1092      // store this broadcast template in the database:
1093      $bid = $this->insertBroadcast($subj, $content, $selector_name);
1094
1095      // we're gonna grab some peops:
1096      $map = $this->prepClass('person');
1097      $cname = $map['classname'];
1098      $objs = $this->getSeriesFromSQL($selector['sql'], $cname);
1099      $attempted = 0;
1100      $succeeded = 0;
1101      $skipped = array();
1102      reset($objs);
1103      while(list(,$obj) = each($objs)) {
1104        $to = $obj->_email;
1105        if (is_null($to) || !is_null($this->isValid('email', $to))) {
1106          $skipped[] = $obj;
1107        } else {
1108          $subject = trim(str_replace("\n", '', $this->applyBroadcastTemplate($subj, $obj)));
1109          $body = trim(wordwrap($this->applyBroadcastTemplate($content, $obj)));
1110          $attempted++;
1111          if ($this->mail('broadcast', $to, $subject, $body, array(), $personal, $bid))
1112            $succeeded++;
1113        }
1114      }
1115      return $succeeded.' out of '.$attempted.' mails sent.'.
1116        (count($skipped) > 0 ? '<br/>Did not try the following people because they had no valid e-mail address: <ul>'.
1117         join('', array_map(create_function('$x', 'return "<li>".$x->getLinkedTitle()."\n";'), $skipped)).'</ul>': '');
1118    }
1119
1120    function getFromAddressSelector($personal = false) {
1121      return '<div>
1122<label><input name="personal" type="radio" value="sitewide" '.($personal ? '' : 'checked').'>'.htmlentities(trim(str_replace("\n", '', $this->getSiteName().' <'.$this->_site_email_from.'>'))).'</label><br />
1123<label><input name="personal" type="radio" value="personal" '.($personal ? 'checked' : '').'>'.htmlentities(trim(str_replace("\n", '', $this->_authenticated_user->getTitle().' <'.$this->_authenticated_user->_email.'>'))).'</label>
1124</div>';
1125    }
1126
1127    function getBroadcastEmailForm() {
1128      $subj = '';
1129      $content = '';
1130      $preview = '';
1131      $ret = '';
1132      $selector_name = '';
1133      $from = trim(str_replace("\n", '', $this->getSiteName().' <'.$this->_site_email_from.'>'));
1134      $personal = false;
1135
1136      if ($_SERVER['REQUEST_METHOD'] == 'POST') {
1137        if ($this->_actually_send_email)
1138          $this->addWarning('Sending e-mail is actually enabled.  Please be careful with this!');
1139        else 
1140          $ret .= '<div>Sending e-mail is currently disabled.  Messages will be logged, but not actually sent.</div>';
1141        $subj = $_POST['subject'];
1142        $content = $_POST['content'];
1143        $personal = (($_POST['personal'] == 'personal') && $this->isAuthenticated());
1144
1145        if ($personal)
1146          $from = trim(str_replace("\n", '', $this->_authenticated_user->getTitle().' <'.$this->_authenticated_user->_email.'>'));
1147         
1148       
1149        $selector_name = $_POST['selector'];
1150        $selectors = $this->getBroadcastSelectors();
1151        if (!array_key_exists($selector_name, $selectors)) {
1152          $this->addWarning('You chose an unavailable selector!');
1153          return '';
1154        }
1155        $selector = $selectors[$selector_name];
1156       
1157        $preview = '<fieldset><legend>Preview Example:</legend>';
1158       
1159        // we're gonna grab a person:
1160        $map = $this->prepClass('person');
1161        $cname = $map['classname'];
1162        $data = $this->getSingletonFromSQL($selector['sql'].' LIMIT 1', false);
1163        if (is_null($data)) {
1164          $preview .= '<span class="error">There are no selectors which match <q>'.$selector['description'].'</q></span>';
1165        } else {
1166          $obj = new $cname(array('data' => $data));
1167          $preview .= '<pre>To: '.$obj->_email.'
1168From: '.htmlentities($from).'
1169Subject: '.htmlentities(trim(str_replace("\n", '', $this->applyBroadcastTemplate($subj, $obj)))).'
1170
1171'.htmlentities(trim(wordwrap($this->applyBroadcastTemplate($content, $obj)))).'</pre>';
1172        }
1173        $preview .= '</fieldset>';
1174      }
1175
1176      return $ret.'<form name="broadcast" action="'.$this->Path('broadcast').'" method="post">
1177'.$this->getFormTokenHiddenInput().'
1178<fieldset><legend>Send to:</legend>
1179'.$this->getBroadcastSelectorInput($selector_name).'
1180</fieldset>
1181'.$preview.'
1182<fieldset><legend>Message Content:</legend>
1183'.$this->getAllowedTemplateLegend().'
1184From: '.$this->getFromAddressSelector($personal).'<br />
1185<label>Subject:<br />
1186<input name="subject" type="text" size="60" value="'.htmlentities($subj).'" /><br />
1187<label>Body:<br />
1188<textarea name="content" rows="20" cols="72">'.htmlentities($content).
1189        '</textarea>
1190</label><br/>
1191<input type="submit" class="submit" name="submit" value="Preview"/>
1192'.($_SERVER['REQUEST_METHOD'] == 'POST' ? '<input class="submit" type="submit" name="submit" value="Send"/>' : '').'
1193</fieldset>
1194</form>';
1195    }
1196
1197    function getMainContentExtended() {
1198      $this->error('nothing but viewing is allowed at the moment.');
1199      return '';
1200    }
1201
1202    function getPageTitle() {
1203      return $this->_page_title;
1204    }
1205    function addStyleSheet($fname) {
1206      if (!in_array($fname, $this->_stylesheets))
1207        $this->_stylesheets[] = $fname;
1208    }
1209    function addSourcedScript($fname) {
1210      if (!in_array($fname, $this->_sourced_scripts))
1211        $this->_sourced_scripts[] = $fname;
1212    }
1213    function addScriptChunk($chunk) {
1214      if (!in_array($fname, $this->_scripts))
1215        $this->_scripts[] = $chunk;
1216    }
1217
1218    function getLinks() {
1219      return join("\n", array_map(create_function('$x', 'global $dkg_site; return "<link rel=\"stylesheet\" href=\"".$dkg_site->Path($x)."\" type=\"text/css\">";'), $this->_stylesheets)).'
1220'.join("\n", array_map(create_function('$x', 'global $dkg_site; return "<script language=\"javascript\" type=\"text/javascript\" src=\"".$dkg_site->Path($x)."\"></script>";'), $this->_sourced_scripts)).'
1221'.join("\n", array_map(create_function('$x', 'global $dkg_site; return "<script language=\"javascript\" type=\"text/javascript\">\n".$x."\n</script>";'), $this->_scripts)).'
1222<link rel="icon" href="'.$this->Path($this->_icon).'" type="image/png" >
1223<link rel="shortcut icon" href="'.$this->Path($this->_icon).'" type="image/png" >'
1224        ;
1225    }
1226   
1227    // adds a tinyMCE default web editor:
1228    function addTinyMCE() {
1229      $this->addSourcedScript('scripts/tiny_mce/tiny_mce.js');
1230      $this->addScriptChunk('tinyMCE.init({
1231        mode : "textareas",
1232        theme : "simple"
1233});');
1234    }
1235
1236    function getWarnings() {
1237      if (count($this->_warnings)) {
1238        return "\n<ul class=\"warnings\">".join('', array_map(create_function('$x', 'return "\n<li>".$x;'), $this->_warnings))."\n</ul>\n";
1239      } else {
1240        return '';
1241      }
1242    }
1243
1244        function renderPage() {
1245      // get this first, because it might populate the scripts, links, icons, warnings, etc.
1246      $body = $this->getPageBody();
1247
1248          return '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1249<html>
1250<head>
1251<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
1252'.
1253        $this->getLinks().'
1254<title>'.
1255        $this->_page_title.
1256        '</title>
1257</head>
1258<body '.(in_array('scripts/dkg.base.js', $this->_sourced_scripts) ? 'onload="DKG.runArray(DKG.onLoadScripts);" ' : '').'>'.
1259        $this->getWarnings().
1260        $body.
1261        '</body>
1262</html>';
1263        }
1264
1265    function insertBroadcast($subj, $body, $selector) {
1266       $this->executeSQL('INSERT INTO broadcast (subj, body, selector, sender_id) VALUES ('.$this->escStr($subj).', '.
1267                         $this->escStr($body).', '.
1268                         $this->escStr($selector).', '.
1269                         (int)$this->_authenticated_user->getID().')');
1270       // return the last id from the log:
1271       return (int)$this->getValueFromSQL('SELECT currval(\'broadcast_id_seq\') AS broadcast_id', 'broadcast_id');
1272    }
1273
1274    function log($data) {
1275       $this->executeSQL('INSERT INTO log (data, backtrace, servervars) VALUES ('.$this->escStr($data).', '.
1276                         $this->escStr(print_r(debug_backtrace(), 1)).', '.
1277                         $this->escStr(print_r($_SERVER, 1)).')', false);
1278       // return the last id from the log:
1279       return (int)$this->getValueFromSQL('SELECT currval(\'log_id_seq\') AS log_id', 'log_id');
1280    }
1281
1282    function error($str) {
1283      $this->log($str);
1284      trigger_error($str, E_USER_ERROR);
1285    }
1286
1287        // stolen blatently from my work on CMOD:
1288    function getSnippet($fname) {
1289      if (preg_match('|[^a-z0-9_-]|', $fname))
1290        $this->error('bad snippet name "'.$fname.'"');
1291     
1292      $zz = dirname(__FILE__).'/snippets/'.$fname.'.html';
1293      if (is_readable($zz)) {
1294        return file_get_contents($zz);
1295      } else {
1296        // check for defaults before failing:
1297        $zz = dirname(__FILE__).'/snippets/'.$fname.'.default.html';
1298        if (is_readable($zz)) {
1299          return file_get_contents($zz);
1300        } else {
1301          $this->error('Failed to getSnippet named "'.$fname.'" at "'.$zz.'".');
1302        }
1303      }
1304    }
1305
1306    function escStr($str) {
1307      return "'".pg_escape_string($str)."'";
1308    }
1309
1310    function intOrDefault($x, $default = 'DEFAULT') {
1311      return ($this->isEmpty($x) ? $default : (int)$x);
1312    }
1313   
1314    function stringOrDefault($x, $default = 'DEFAULT') {
1315      return ($this->isEmpty($x) ? $default : $this->escStr($x));
1316    }
1317
1318    function boolOrDefault($x, $default = 'DEFAULT') {
1319      return ($this->isEmpty($x) ? $default : 
1320              (('t' === $x) || ('true' === $x) || (true === $x) || (1 === $x)) ? 'true' : 'false');
1321    }
1322
1323    function defaultOnEmpty($x, $default) {
1324      return $this->isEmpty($x) ? $default : $x;
1325    }
1326
1327    function nonEmptyString($x) {
1328      return ('' === $x ? NULL : $x);
1329    }
1330
1331    // stolen from my work on CMOD.
1332    function unmagicquotes($zz) {
1333      if (get_magic_quotes_gpc()) {
1334        if (is_string($zz)) {
1335          return stripslashes($zz);
1336        } elseif (is_array($zz)) {
1337          $ret = array();
1338          reset($zz);
1339          while (list($k,$v) = each($zz)) {
1340            $ret[$k] = $this->unmagicquotes($v);
1341          }
1342          return $ret;
1343        } else {
1344          return $zz;
1345        }
1346      } else {
1347        return $zz;
1348      }
1349    }
1350
1351
1352    function getAll($x, $extension = '') {
1353      $map = $this->prepClass($x);
1354      return $this->getSeriesFromSQL('SELECT '.$map['table'].'.* FROM '.$map['table'].' '.
1355                                     (((is_null($extension) || '' == $extension) && array_key_exists('limit', $map)) ? 
1356                                      ' WHERE '.$map['limit'].'_id = '.$this->_active_event  : $extension).
1357                                     ($this->isEmpty($map['sort']) ? '' : ' ORDER BY '.$map['sort']), $map['classname']);
1358    }
1359   
1360    function executeSQL($sql, $requirevalid = true) {
1361      $ret = pg_query($this->_db, $sql);
1362      if (FALSE === $ret) {
1363        if (error_reporting() != 0) {
1364          print pg_last_error();
1365          print '<pre>'.$sql.'</pre>';
1366        }
1367        if ($requirevalid) $this->error('Failed to run query "'.$sql.'"');
1368      }
1369      return $ret;
1370    }
1371
1372    function Path() {
1373      $args = func_get_args();
1374      return $this->_base_path.join('/', $args);
1375    }
1376
1377    // text filtering functions:
1378
1379    function filterBlock($str, $fortextarea=false) {
1380      // preg_replace strips attributes from tags, and converts multi-line breaks with <p> tags.
1381      $str = preg_replace(':(<\/?)(\w+)[^>]*>:', '$1$2>', strip_tags($str, '<blockquote><cite><br><ul><ol><li><p><em><strong>'));
1382      if (!$fortextarea) 
1383        $str = preg_replace(':(\r?\n){2,}:', "\n<p>\n", strip_tags($str, '<blockquote><cite><br><ul><ol><li><p><em><strong>'));
1384      return $str;
1385    }
1386
1387
1388    // SQL manipulation functions:
1389   
1390    function getSingletonFromSQL($sql, $require = true) {
1391      $res = $this->executeSQL($sql);
1392     
1393      $ret = pg_fetch_assoc($res);
1394      if (false === $ret) {
1395        if ($require)
1396          $this->error('No entries exist in the database that match "'.$sql.'"');
1397        else
1398          return NULL;
1399      }
1400      if (false === pg_fetch_assoc($res)) {
1401        pg_free_result($res);         
1402        return $ret;
1403      } else {
1404        $this->error('More than one entry matched "'.$sql.'"');
1405        return NULL;
1406      }
1407    }
1408   
1409
1410    function &getValuesFromSQL($sql, $val) {
1411      $res = $this->executeSQL($sql);
1412     
1413      $ret = array();
1414      while ($rr = pg_fetch_assoc($res)) {
1415        $ret[] = $rr[$val];
1416      }
1417      pg_free_result($res);
1418      return $ret;
1419    }
1420
1421    function getValueFromSQL($sql, $val) {
1422      $zz = $this->getSingletonFromSQL($sql);
1423      if (!array_key_exists($val,$zz)) {
1424        $this->error('desired value "'.$val.'" does not exists in return of SQL function "'.$sql.'"');
1425      } else {                     
1426        return $zz[$val];
1427      }
1428    }
1429
1430    function &getKeyValuePairsFromSQL($sql, $keyname, $valuename) {
1431      $res = $this->executeSQL($sql);
1432     
1433      $ret = array();
1434      while ($rr = pg_fetch_assoc($res)) {
1435        $ret[$rr[$keyname]] = $rr[$valuename];
1436      }
1437      pg_free_result($res);
1438      return $ret;
1439    }
1440    function &getSeriesFromSQL($sql, $classname = NULL) {
1441      $res = $this->executeSQL($sql);
1442      $ret = array();
1443      while ($rr = pg_fetch_assoc($res)) {
1444        if (is_null($classname)) {
1445          $ret[] = $rr;
1446        } else {
1447          $ret[] = new $classname(array('data' => $rr));
1448        }
1449      }
1450      pg_free_result($res);
1451      return $ret;
1452    }
1453    function &getMultiSeriesFromSQLByValue($sql, $indexvalue, $classname = NULL) {
1454      $res = $this->executeSQL($sql);
1455      $ret = array();
1456      while ($rr = pg_fetch_assoc($res)) {
1457        if (!array_key_exists($rr[$indexvalue], $ret))
1458          $rr[$rr[$indexvalue]] = array();
1459        if (is_null($classname)) {
1460          $ret[$rr[$indexvalue]][] = $rr;
1461        } else {
1462          $ret[$rr[$indexvalue]][] = new $classname(array('data' => $rr));
1463        }
1464      }
1465      pg_free_result($res);
1466      return $ret;
1467    }
1468    function &getHashFromSQL($sql, $keyname, $classname = NULL) {
1469      $res = $this->executeSQL($sql);
1470      $ret = array();
1471      while ($rr = pg_fetch_assoc($res)) {
1472        if (is_null($classname)) {
1473          $ret[$rr[$keyname]] = $rr;
1474        } else {
1475          $ret[$rr[$keyname]] = new $classname(array('data' => $rr));
1476        }
1477      }
1478      pg_free_result($res);
1479      return $ret;
1480    }
1481  }
1482}
1483global $dkg_site;
1484?>
Note: See TracBrowser for help on using the repository browser.