source: trunk/services/admins.php @ 408

Last change on this file since 408 was 408, checked in by anonymous, 12 years ago

Improved admin list filter query retention

File size: 22.7 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
24 * admins.php
25 */
26
27// require_once dirname(__FILE__) . '/_config.inc.php';
28
29$auth->requireLogin();
30$app->sslOn();
31
32require_once 'codebase/lib/PageNumbers.inc.php';
33require_once 'codebase/lib/Cache.inc.php';
34require_once 'codebase/lib/FormValidator.inc.php';
35require_once 'codebase/lib/SortOrder.inc.php';
36require_once 'codebase/lib/TemplateGlue.inc.php';
37require_once 'codebase/lib/Prefs.inc.php';
38require_once 'codebase/lib/Lock.inc.php';
39require_once 'codebase/lib/Version.inc.php';
40
41
42/********************************************************************
43* CONFIG
44********************************************************************/
45
46// Titles and navigation header.
47$nav->add(_("Administrators"), null);
48
49// The object to validate form input.
50$fv = new FormValidator();
51
52// Configure the prefs object.
53$tmp_prefs = new Prefs('admins');
54$tmp_prefs->setParam(array('persistent' => false));
55
56// Configure the cache object.
57$cache = new Cache('admins');
58$cache->setParam(array('enabled' => false)); // Better leave disabled; the list gets out of sync with the db otherwise, somehow.
59
60// Instantiate a sorting object with the default sort and order. Add SQL for each column.
61$so = new SortOrder('admin_id', 'DESC');
62$so->setColumn('admin_id', $auth->getParam('db_primary_key') . ' ASC', $auth->getParam('db_primary_key') . ' DESC');
63$so->setColumn('username', $auth->getParam('db_username_column') . ' ASC', $auth->getParam('db_username_column') . ' DESC');
64$so->setColumn('userpass', $auth->getParam('db_table') . '.userpass ASC', $auth->getParam('db_table') . '.userpass DESC');
65$so->setColumn('first_name', $auth->getParam('db_table') . '.first_name ASC', $auth->getParam('db_table') . '.first_name DESC');
66$so->setColumn('last_name', $auth->getParam('db_table') . '.last_name ASC', $auth->getParam('db_table') . '.last_name DESC');
67$so->setColumn('email', $auth->getParam('db_table') . '.email ASC', $auth->getParam('db_table') . '.email DESC');
68$so->setColumn('user_type', $auth->getParam('db_table') . '.user_type ASC', $auth->getParam('db_table') . '.user_type DESC');
69$so->setColumn('seconds_online', $auth->getParam('db_table') . '.seconds_online ASC', $auth->getParam('db_table') . '.seconds_online DESC');
70$so->setColumn('last_login_datetime', $auth->getParam('db_table') . '.last_login_datetime ASC', $auth->getParam('db_table') . '.last_login_datetime DESC');
71$so->setColumn('last_access_datetime', $auth->getParam('db_table') . '.last_access_datetime ASC', $auth->getParam('db_table') . '.last_access_datetime DESC');
72$so->setColumn('last_login_ip', $auth->getParam('db_table') . '.last_login_ip ASC', $auth->getParam('db_table') . '.last_login_ip DESC');
73$so->setColumn('added_by_user_id', $auth->getParam('db_table') . '.added_by_user_id ASC', $auth->getParam('db_table') . '.added_by_user_id DESC');
74$so->setColumn('modified_by_user_id', $auth->getParam('db_table') . '.modified_by_user_id ASC', $auth->getParam('db_table') . '.modified_by_user_id DESC');
75$so->setColumn('added_datetime', $auth->getParam('db_table') . '.added_datetime ASC', $auth->getParam('db_table') . '.added_datetime DESC');
76$so->setColumn('modified_datetime', $auth->getParam('db_table') . '.modified_datetime ASC', $auth->getParam('db_table') . '.modified_datetime DESC');
77
78// Instantiate page numbers. Total items are set and calculation is done in the getRecordList function.
79$page = new PageNumbers();
80$page->setPerPage(getFormData('per_page'), 50);
81$page->setPageNumber(getFormData('page_number'));
82
83// Query parameters to retain only locally.
84$locally_carried_queries = array(
85    'search_query',
86);
87
88/********************************************************************
89* MAIN
90********************************************************************/
91
92// We may want to use the add/edit interface from another script, so this
93// allows us to remember which page we came from so we can go back there.
94if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
95    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'admins');
96}
97
98if (getFormData('break_list_cache', false)) {
99    // Remove any stale cached list data.
100    $cache->delete('list');
101}
102
103// What action to take.
104switch (getFormData('op')) {
105
106case 'add' :
107    // Initialize variables for the form template.
108    $frm =& addRecordForm();
109    $nav->add(_("Add Administrator"));
110    $main_template = 'admin_form.ihtml';
111    break;
112
113case 'edit' :
114    // Initialize variables for the form template.
115    $frm =& editRecordForm(getFormData('admin_id'));
116    $nav->add(_("Edit Administrator"));
117    $main_template = 'admin_form.ihtml';
118    break;
119
120case 'del' :
121    deleteRecord(getFormData('admin_id'));
122    if ($app->validBoomerangURL('admins')) {
123        // Display boomerang page.
124        $app->dieBoomerangURL('admins', $locally_carried_queries);
125    }
126    // Display default page.
127    $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
128    break;
129
130case 'insert' :
131    if (getFormdata('cancel', false)) {
132        if ($app->validBoomerangURL('admins')) {
133            // Display boomerang page.
134            $app->dieBoomerangURL('admins', $locally_carried_queries);
135        }
136        // Display default page.
137        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
138    }
139    validateInput();
140    if ($fv->anyErrors()) {
141        $frm =& addRecordForm();
142        $frm = array_merge($frm, getFormData());
143        $nav->add(_("Add Administrator"));
144        $main_template = 'admin_form.ihtml';
145    } else {
146        $admin_id = insertRecord(getFormData());
147        if (getFormdata('repeat', false)) {
148            // Display function again.
149            $app->dieURL($_SERVER['PHP_SELF'] . '?op=add', $locally_carried_queries);
150        } else if ($app->validBoomerangURL('admins')) {
151            // Display boomerang page.
152            $app->dieBoomerangURL('admins', $locally_carried_queries);
153        }
154        // Display default page.
155        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
156    }
157    break;
158
159case 'update' :
160    if (getFormdata('reset', false)) {
161        $app->raiseMsg(_("Saved values have been reloaded."), MSG_NOTICE, __FILE__, __LINE__);
162        $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . getFormData('admin_id'), $locally_carried_queries);
163    }
164    if (getFormdata('cancel', false)) {
165        // Remove lock
166        $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), getFormData('admin_id'));
167        $lock->remove();
168        if ($app->validBoomerangURL('admins')) {
169            // Display boomerang page.
170            $app->dieBoomerangURL('admins', $locally_carried_queries);
171        }
172        // Display default page.
173        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
174    }
175    validateInput();
176    if ($fv->anyErrors()) {
177        $frm =& editRecordForm(getFormData('admin_id'));
178        $frm = array_merge($frm, getFormData());
179        $nav->add(_("Edit Administrator"));
180        $main_template = 'admin_form.ihtml';
181    } else {
182        updateRecord(getFormData());
183        if (getFormdata('repeat', false)) {
184            // Display edit function with next available ID.
185            $qid = $db->query("SELECT " . $auth->getParam('db_primary_key') . " FROM " . $auth->getParam('db_table') . " WHERE " . $auth->getParam('db_primary_key') . " > '" . $db->escapeString(getFormData('admin_id')) . "' ORDER BY " . $auth->getParam('db_primary_key') . " ASC LIMIT 1");
186            if (list($next_id) = mysql_fetch_row($qid)) {
187                $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $next_id, $locally_carried_queries);
188            } else {
189                $app->raiseMsg(_("Cannot edit next, the end of the list was reached"), MSG_NOTICE, __FILE__, __LINE__);
190            }
191        } else if ($app->validBoomerangURL('admins')) {
192            // Display boomerang page.
193            $app->dieBoomerangURL('admins', $locally_carried_queries);
194        }
195        // Display default page.
196        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
197    }
198    break;
199
200default :
201    $list =& getRecordList();
202    $main_template = 'admin_list.ihtml';
203    break;
204}
205
206/******************************************************************************
207 * TEMPLATE INITIALIZATION
208 *****************************************************************************/
209
210include 'header.ihtml';
211$app->carryQuery($locally_carried_queries);
212include 'codebase/services/templates/' . $main_template;
213include 'footer.ihtml';
214
215/********************************************************************
216* FUNCTIONS
217********************************************************************/
218
219
220function validateInput()
221{
222    global $fv, $auth;
223
224    // If the username was changed during edit, verify.
225    if (((getFormData('username') != getFormData('old_username')) && 'update' == getFormData('op'))
226    || 'insert' == getFormData('op')) {
227        if ($auth->usernameExists(getFormData('username'))) {
228            $fv->addError('username', sprintf(_("The username %s already exists. Please choose another."), getFormData('username')));
229        }
230    }
231
232    if (getFormData('user_type') == 'root' && 'root' != $auth->get('user_type')) {
233        $fv->addError('user_type', sprintf(_("You do not have clearance to create a user with root privileges."), null));
234    }
235
236    $fv->numericRange('admin_id', -32768, 32767, _("<strong>Admin id</strong> must be a valid number between -32768 and 32767."));
237
238    $fv->isEmpty('username', _("<strong>Username</strong> cannot be blank."));
239    $fv->stringLength('username', 0, 255, _("<strong>Username</strong> must contain less than 256 characters."));
240
241    $fv->isEmpty('userpass', _("<strong>Passwords</strong> cannot be blank."));
242    $fv->stringLength('userpass', 6, 36, _("<strong>Passwords</strong> must be between 6 and 36 characters long."));
243
244    $fv->stringLength('first_name', 0, 255, _("<strong>First name</strong> must contain less than 256 characters."));
245
246    $fv->stringLength('last_name', 0, 255, _("<strong>Last name</strong> must contain less than 256 characters."));
247
248    $fv->isEmpty('email', _("<strong>Email</strong> cannot be blank."));
249    $fv->stringLength('email', 0, 255, _("<strong>Email</strong> must contain less than 256 characters."));
250    $fv->validateEmail('email');
251
252    $fv->isEmpty('user_type', _("<strong>User type</strong> cannot be blank."));
253    $fv->stringLength('user_type', 0, 255, _("<strong>User type</strong> has an invalid selection."));
254}
255
256function &addRecordForm()
257{
258    // Set default values for the reset of the fields.
259    $frm = array(
260        'admin_id' => '',
261        'old_username' => '',
262        'username' => '',
263        'userpass' => '',
264        'first_name' => '',
265        'last_name' => '',
266        'email' => '',
267        'user_type' => '',
268        'seconds_online' => '0',
269        'last_login_datetime' => '0000-00-00 00:00:00',
270        'last_access_datetime' => '0000-00-00 00:00:00',
271        'last_login_ip' => '0.0.0.0',
272        'added_by_user_id' => '',
273        'modified_by_user_id' => '',
274        'added_datetime' => '0000-00-00 00:00:00',
275        'modified_datetime' => '0000-00-00 00:00:00',
276        'new_op' => 'insert',
277        'submit_buttons' => array(
278            'submit' => _("Add Administrator"),
279            'repeat' => _("Add &amp; repeat"),
280            'cancel' => _("Cancel"),
281        ),
282    );
283
284    return $frm;
285}
286
287function &editRecordForm($id)
288{
289    global $auth;
290    global $lock;
291    $app =& App::getInstance();
292    $db =& DB::getInstance();
293   
294    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id);
295    if ($lock->isLocked() && !$lock->isMine()) {
296        $lock->dieErrorPage();
297    }
298
299    // Get the information for the form.
300    $qid = $db->query("
301        SELECT *,
302        " . $auth->getParam('db_primary_key') . " AS admin_id
303        FROM " . $auth->getParam('db_table') . "
304        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'
305    ");
306    if (!$frm = mysql_fetch_assoc($qid)) {
307        $app->logMsg('Could not find record with admin_id: ' . $id, LOG_WARNING, __FILE__, __LINE__);
308        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
309        $app->dieBoomerangURL('admins', $locally_carried_queries);
310    }
311
312    // Lock this record.
313    $lock->set($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id, $frm['username']);
314
315    // Set misc values for the form.
316    $frm = array_merge(array(
317        'admin_id' => '',
318        'old_username' => $frm['username'],
319        'username' => '',
320//         'userpass' => '****************',
321        'first_name' => '',
322        'last_name' => '',
323        'email' => '',
324        'user_type' => '',
325        'seconds_online' => '0',
326        'last_login_datetime' => '0000-00-00 00:00:00',
327        'last_access_datetime' => '0000-00-00 00:00:00',
328        'last_login_ip' => '0.0.0.0',
329        'added_by_user_id' => '',
330        'modified_by_user_id' => '',
331        'added_datetime' => '0000-00-00 00:00:00',
332        'modified_datetime' => '0000-00-00 00:00:00',
333        'new_op' => 'update',
334        'old_username' => $frm['username'],
335        'submit_buttons' => array(
336            'submit' => _("Save changes"),
337            'repeat' => _("Save &amp; edit next"),
338            'reset' => _("Reset"),
339            'cancel' => _("Cancel"),
340        ),
341    ), $frm, array('userpass' => '****************'));
342
343    return $frm;
344}
345
346function deleteRecord($id)
347{
348    global $auth;
349    global $lock;
350    global $cache;
351    $app =& App::getInstance();
352    $db =& DB::getInstance();
353   
354    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id);
355    if ($lock->isLocked() && !$lock->isMine()) {
356        $lock->dieErrorPage();
357    }
358
359    // Remove any stale cached list data.
360    $cache->delete('list');
361
362    // Get the information for this object.
363    $qid = $db->query("
364        SELECT " . $auth->getParam('db_username_column') . ", user_type from " . $auth->getParam('db_table') . "
365        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'
366    ");
367    if (! list($name, $user_type) = mysql_fetch_row($qid)) {
368        $app->logMsg('Could not find record with admin_id: ' . $id, LOG_WARNING, __FILE__, __LINE__);
369        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
370        $app->dieBoomerangURL('admins', $locally_carried_queries);
371    }
372
373    // Get the information for this object.
374    $qid = $db->query("SELECT COUNT(*) from " . $auth->getParam('db_table') . "");
375    list($num_admins) = mysql_fetch_row($qid);
376    if ('root' == $user_type && 'root' != $auth->get('user_type')) {
377        // Only root users can delete root users!
378        $app->raiseMsg(_("You do not have clearance to delete a root administrator."), MSG_NOTICE, __FILE__, __LINE__);
379    } else if ($num_admins <= 1) {
380        // There must always be at least one admnistrator!
381        $app->raiseMsg(_("You cannot delete the only administrator in the database. There must be at least one to log in and create other users."), MSG_NOTICE, __FILE__, __LINE__);
382    } else if ($auth->get('user_id') == $id) {
383        // Do not delete yourself!
384        $app->raiseMsg(_("You cannot delete yourself."), MSG_NOTICE, __FILE__, __LINE__);
385    } else {
386        // Delete the record.
387        $db->query("DELETE FROM " . $auth->getParam('db_table') . " WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'");
388        $app->raiseMsg(sprintf(_("The admin <em>%s</em> has been deleted."), $name), MSG_SUCCESS, __FILE__, __LINE__);
389    }
390
391    // Unlock record.
392    $lock->remove();
393}
394
395function insertRecord($frm)
396{
397    global $auth;
398    global $cache;
399    $app =& App::getInstance();
400    $db =& DB::getInstance();
401   
402    // Remove any stale cached list data.
403    $cache->delete('list');
404
405    // Insert record data.
406    $db->query("
407        INSERT INTO " . $auth->getParam('db_table') . " (
408            " . $auth->getParam('db_username_column') . ",
409            first_name,
410            last_name,
411            email,
412            user_type,
413            added_by_user_id,
414            added_datetime
415        ) VALUES (
416            '" . $db->escapeString($frm['username']) . "',
417            '" . $db->escapeString($frm['first_name']) . "',
418            '" . $db->escapeString($frm['last_name']) . "',
419            '" . $db->escapeString($frm['email']) . "',
420            '" . $db->escapeString($frm['user_type']) . "',
421            '" . $db->escapeString($auth->get('user_id')) . "',
422            NOW()
423        )
424    ");
425    $last_insert_id = mysql_insert_id($db->getDBH());
426
427    // Set admin password.
428    $auth->setPassword($last_insert_id, $frm['userpass']);
429
430    // Create version.
431    $version = Version::getInstance($auth);
432    $version->create($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $last_insert_id, $frm['username']);
433
434    $app->raiseMsg(sprintf(_("The Administrator <em>%s</em> has been added."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
435
436    return $last_insert_id;
437}
438
439function updateRecord($frm)
440{
441    global $auth;
442    global $lock;
443    global $cache;
444    $app =& App::getInstance();
445    $db =& DB::getInstance();
446   
447    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $frm['admin_id']);
448    if ($lock->isLocked() && !$lock->isMine()) {
449        $lock->dieErrorPage();
450    }
451
452    // Remove any stale cached list data.
453    $cache->delete('list');
454
455    // If the userpass is left blank or with the filler **** characters, we don't want to update it.
456    if (!empty($frm['userpass']) && !preg_match('/[\*]{4,}/', $frm['userpass'])) {
457        // Set user password.
458        $auth->setPassword($frm['admin_id'], $frm['userpass']);
459    }
460
461    // Update record data.
462    $db->query("
463        UPDATE " . $auth->getParam('db_table') . " SET
464            " . $auth->getParam('db_username_column') . " = '" . $db->escapeString($frm['username']) . "',
465            first_name = '" . $db->escapeString($frm['first_name']) . "',
466            last_name = '" . $db->escapeString($frm['last_name']) . "',
467            email = '" . $db->escapeString($frm['email']) . "',
468            user_type = '" . $db->escapeString($frm['user_type']) . "',
469            modified_by_user_id = '" . $db->escapeString($auth->get('user_id')) . "',
470            modified_datetime = NOW()
471        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($frm['admin_id']) . "'
472    ");
473
474    // Create version.
475    $version = Version::getInstance($auth);
476    $version->create($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $frm['admin_id'], $frm['username']);
477
478    $app->raiseMsg(sprintf(_("The Administrator <em>%s</em> has been updated."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
479
480    // Unlock record.
481    $lock->remove();
482}
483
484function &getRecordList()
485{
486    global $page;
487    global $so;
488    global $tmp_prefs;
489    global $cache;
490    global $auth;
491    $db =& DB::getInstance();
492   
493    $where_clause = '';
494
495    // Build search query if available.
496    if (getFormData('search_query', false)) {
497        $qry_words = preg_split('/[^\w]/', getFormData('search_query'));
498        for ($i=0; $i<sizeof($qry_words); $i++) {
499            $where_clause .= (empty($where_clause) ? 'WHERE' : 'AND') . "
500                (
501                    " . $auth->getParam('db_table') . "." . $auth->getParam('db_username_column') . " LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
502                    OR " . $auth->getParam('db_table') . ".first_name LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
503                    OR " . $auth->getParam('db_table') . ".last_name LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
504                    OR " . $auth->getParam('db_table') . ".email LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
505                )
506            ";
507        }
508    }
509
510    // Count the total number of records so we can do something about the page numbers.
511    $qid = $db->query("
512        SELECT COUNT(*)
513        FROM " . $auth->getParam('db_table') . "
514        $where_clause
515    ");
516    list($num_results) = mysql_fetch_row($qid);
517
518    // Set page numbers now we know (needed for next step).
519    $page->setTotalItems($num_results);
520    $page->calculate();
521
522    // Final SQL, with sort and page limiters.
523    $sql = "
524        SELECT
525            " . $auth->getParam('db_table') . ".*,
526            " . $auth->getParam('db_table') . "." . $auth->getParam('db_primary_key') . " AS admin_id,           
527            a1." . $auth->getParam('db_username_column') . " AS added_admin_username,
528            a2." . $auth->getParam('db_username_column') . " AS modified_admin_username
529        FROM " . $auth->getParam('db_table') . "
530        LEFT JOIN " . $auth->getParam('db_table') . " a1 ON (" . $auth->getParam('db_table') . ".added_by_user_id = a1." . $auth->getParam('db_primary_key') . ")
531        LEFT JOIN " . $auth->getParam('db_table') . " a2 ON (" . $auth->getParam('db_table') . ".modified_by_user_id = a2." . $auth->getParam('db_primary_key') . ")
532        $where_clause
533        " . $so->getSortOrderSQL() . "
534        " . $page->getLimitSQL() . "
535    ";
536
537    // Use a cash hash to determine if the result-set has changed.
538    // A unique key for this query, with the total_items in case db records
539    // were added since the last cache. This identifies a unique set of
540    // cached data, but we must refer to the list that is cached by a more
541    // generic name. so that we can flush the cache (if records updated)
542    // without knowing the hash.
543    $cache_hash = md5($sql . '|' . $page->total_items);
544    if ($tmp_prefs->get('cache_hash') != $cache_hash) {
545        $cache->delete('list');
546        $tmp_prefs->set('cache_hash', $cache_hash);
547    }
548
549    // First try to return from the cache.
550    if ($cache->exists('list')) {
551        $list = $cache->get('list');
552        return $list;
553    }
554   
555    // The list was not cached, so issue the real query.
556    $qid = $db->query($sql);
557    while ($row = mysql_fetch_assoc($qid)) {
558        $list[] = $row;
559    }
560
561    // Save this list into the cache.
562    if (isset($list) && !empty($list)) {
563        $cache->set('list', $list);
564    }
565
566    return $list;
567}
568
569?>
Note: See TracBrowser for help on using the repository browser.