1<?php
2
3/**
4 +-----------------------------------------------------------------------+
5 | This file is part of the Roundcube Webmail client                     |
6 |                                                                       |
7 | Copyright (C) The Roundcube Dev Team                                  |
8 |                                                                       |
9 | Licensed under the GNU General Public License version 3 or            |
10 | any later version with exceptions for skins & plugins.                |
11 | See the README file for a full license statement.                     |
12 |                                                                       |
13 | PURPOSE:                                                              |
14 |   This class represents a system user linked and provides access      |
15 |   to the related database records.                                    |
16 +-----------------------------------------------------------------------+
17 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18 | Author: Aleksander Machniak <alec@alec.pl>                            |
19 +-----------------------------------------------------------------------+
20*/
21
22/**
23 * Class representing a system user
24 *
25 * @package    Framework
26 * @subpackage Core
27 */
28class rcube_user
29{
30    /** @var int User identifier */
31    public $ID;
32
33    /** @var array User properties */
34    public $data;
35
36    /** @var string User language code */
37    public $language;
38
39    /** @var array User preferences */
40    public $prefs;
41
42
43    /** @var rcube_db Holds database connection */
44    private $db;
45
46    /** @var rcube Framework object */
47    private $rc;
48
49    /** @var array Internal identities cache */
50    private $identities = [];
51
52    /** @var array Internal emails cache */
53    private $emails;
54
55
56    const SEARCH_ADDRESSBOOK = 1;
57    const SEARCH_MAIL        = 2;
58
59
60    /**
61     * Object constructor
62     *
63     * @param int   $id      User id
64     * @param array $sql_arr SQL result set
65     */
66    function __construct($id = null, $sql_arr = null)
67    {
68        $this->rc = rcube::get_instance();
69        $this->db = $this->rc->get_dbh();
70
71        if ($id && !$sql_arr) {
72            $sql_result = $this->db->query(
73                "SELECT * FROM " . $this->db->table_name('users', true)
74                . " WHERE `user_id` = ?", $id
75            );
76
77            $sql_arr = $this->db->fetch_assoc($sql_result);
78        }
79
80        if (!empty($sql_arr)) {
81            $this->ID       = (int) $sql_arr['user_id'];
82            $this->data     = $sql_arr;
83            $this->language = $sql_arr['language'];
84        }
85    }
86
87    /**
88     * Build a user name string (as e-mail address)
89     *
90     * @param string $part Username part (empty or 'local' or 'domain', 'mail')
91     *
92     * @return string Full user name or its part
93     */
94    function get_username($part = null)
95    {
96        if (!empty($this->data['username'])) {
97            // return real name
98            if (!$part) {
99                return $this->data['username'];
100            }
101
102            list($local, $domain) = rcube_utils::explode('@', $this->data['username']);
103
104            // at least we should always have the local part
105            if ($part == 'local') {
106                return $local;
107            }
108            // if no domain was provided...
109            if (empty($domain)) {
110                $domain = $this->rc->config->mail_domain($this->data['mail_host']);
111            }
112
113            if ($part == 'domain') {
114                return $domain;
115            }
116
117            if (!empty($domain)) {
118                return $local . '@' . $domain;
119            }
120
121            return $local;
122        }
123    }
124
125    /**
126     * Get the preferences saved for this user
127     *
128     * @return array Hash array with prefs
129     */
130    function get_prefs()
131    {
132        if (isset($this->prefs)) {
133            return $this->prefs;
134        }
135
136        $this->prefs = [];
137
138        if (!empty($this->language)) {
139            $this->prefs['language'] = $this->language;
140        }
141
142        if ($this->ID) {
143            // Preferences from session (write-master is unavailable)
144            if (!empty($_SESSION['preferences'])) {
145                // Check last write attempt time, try to write again (every 5 minutes)
146                if ($_SESSION['preferences_time'] < time() - 5 * 60) {
147                    $saved_prefs = unserialize($_SESSION['preferences']);
148                    $this->rc->session->remove('preferences');
149                    $this->rc->session->remove('preferences_time');
150                    $this->save_prefs($saved_prefs);
151                }
152                else {
153                    $this->data['preferences'] = $_SESSION['preferences'];
154                }
155            }
156
157            if ($this->data['preferences']) {
158                $this->prefs += (array) unserialize($this->data['preferences']);
159            }
160        }
161
162        return $this->prefs;
163    }
164
165    /**
166     * Write the given user prefs to the user's record
167     *
168     * @param array $a_user_prefs User prefs to save
169     * @param bool  $no_session   Simplified language/preferences handling
170     *
171     * @return bool True on success, False on failure
172     */
173    function save_prefs($a_user_prefs, $no_session = false)
174    {
175        if (!$this->ID) {
176            return false;
177        }
178
179        $config       = $this->rc->config;
180        $transient    = $config->transient_options();
181        $a_user_prefs = array_diff_key($a_user_prefs, array_flip($transient));
182
183        if (empty($a_user_prefs)) {
184            return true;
185        }
186
187        $plugin = $this->rc->plugins->exec_hook('preferences_update', [
188                'userid' => $this->ID,
189                'prefs'  => $a_user_prefs,
190                'old'    => (array)$this->get_prefs()
191        ]);
192
193        if (!empty($plugin['abort'])) {
194            return false;
195        }
196
197        $a_user_prefs = $plugin['prefs'];
198        $old_prefs    = $plugin['old'];
199        $defaults     = $config->all();
200
201        // merge (partial) prefs array with existing settings
202        $this->prefs = $save_prefs = $a_user_prefs + $old_prefs;
203        unset($save_prefs['language']);
204
205        // don't save prefs with default values if they haven't been changed yet
206        // Warning: we use result of rcube_config::all() here instead of just get() (#5782)
207        foreach ($a_user_prefs as $key => $value) {
208            if ($value === null || (!isset($old_prefs[$key]) && isset($defaults[$key]) && $value === $defaults[$key])) {
209                unset($save_prefs[$key]);
210            }
211        }
212
213        $save_prefs = serialize($save_prefs);
214        if (!$no_session) {
215            $this->language = $_SESSION['language'];
216        }
217
218        $this->db->query(
219            "UPDATE ".$this->db->table_name('users', true).
220            " SET `preferences` = ?, `language` = ?".
221            " WHERE `user_id` = ?",
222            $save_prefs,
223            $this->language,
224            $this->ID
225        );
226
227        // Update success
228        if ($this->db->affected_rows() !== false) {
229            $this->data['preferences'] = $save_prefs;
230
231            if (!$no_session) {
232                $config->set_user_prefs($this->prefs);
233
234                if (isset($_SESSION['preferences'])) {
235                    $this->rc->session->remove('preferences');
236                    $this->rc->session->remove('preferences_time');
237                }
238            }
239
240            return true;
241        }
242        // Update error, but we are using replication (we have read-only DB connection)
243        // and we are storing session not in the SQL database
244        // we can store preferences in session and try to write later (see get_prefs())
245        else if (!$no_session && $this->db->is_replicated()
246            && $config->get('session_storage', 'db') != 'db'
247        ) {
248            $_SESSION['preferences'] = $save_prefs;
249            $_SESSION['preferences_time'] = time();
250            $config->set_user_prefs($this->prefs);
251            $this->data['preferences'] = $save_prefs;
252        }
253
254        return false;
255    }
256
257    /**
258     * Generate a unique hash to identify this user with
259     */
260    function get_hash()
261    {
262        $prefs = $this->get_prefs();
263
264        // generate a random hash and store it in user prefs
265        if (empty($prefs['client_hash'])) {
266            $prefs['client_hash'] = rcube_utils::random_bytes(16);
267            $this->save_prefs(['client_hash' => $prefs['client_hash']]);
268        }
269
270        return $prefs['client_hash'];
271    }
272
273    /**
274     * Return a list of all user emails (from identities)
275     *
276     * @param bool $default Return only default identity
277     *
278     * @return array List of emails (identity_id, name, email) or single email-data
279     */
280    function list_emails($default = false)
281    {
282        if ($this->emails === null) {
283            $this->emails = [];
284
285            $sql_result = $this->db->query(
286                "SELECT `identity_id`, `name`, `email`"
287                ." FROM " . $this->db->table_name('identities', true)
288                ." WHERE `user_id` = ? AND `del` <> 1"
289                ." ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
290                $this->ID
291            );
292
293            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
294                $this->emails[] = $sql_arr;
295            }
296        }
297
298        return $default ? $this->emails[0] : $this->emails;
299    }
300
301    /**
302     * Get default identity of this user
303     *
304     * @param int $id Identity ID. If empty, the default identity is returned
305     *
306     * @return array Hash array with all cols of the identity record
307     */
308    function get_identity($id = null)
309    {
310        $id = (int) $id;
311
312        // cache identities for better performance
313        if (!array_key_exists($id, $this->identities)) {
314            $result = $this->list_identities($id ? "AND `identity_id` = $id" : '');
315            $this->identities[$id] = $result[0];
316        }
317
318        return $this->identities[$id];
319    }
320
321    /**
322     * Return a list of all identities linked with this user
323     *
324     * @param string $sql_add   Optional WHERE clauses
325     * @param bool   $formatted Format identity email and name
326     *
327     * @return array List of identities
328     */
329    function list_identities($sql_add = '', $formatted = false)
330    {
331        $result = [];
332
333        $sql_result = $this->db->query(
334            "SELECT * FROM ".$this->db->table_name('identities', true)
335            . " WHERE `del` <> 1 AND `user_id` = ?" . ($sql_add ? " $sql_add" : "")
336            . " ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
337            $this->ID
338        );
339
340        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
341            if ($formatted) {
342                $ascii_email = format_email($sql_arr['email']);
343                $utf8_email  = format_email(rcube_utils::idn_to_utf8($ascii_email));
344
345                $sql_arr['email_ascii'] = $ascii_email;
346                $sql_arr['email']       = $utf8_email;
347                $sql_arr['ident']       = format_email_recipient($ascii_email, $sql_arr['name']);
348            }
349
350            $result[] = $sql_arr;
351        }
352
353        return $result;
354    }
355
356    /**
357     * Update a specific identity record
358     *
359     * @param int   $iid  Identity ID
360     * @param array $data Hash array with col->value pairs to save
361     *
362     * @return bool True if saved successfully, false if nothing changed
363     */
364    function update_identity($iid, $data)
365    {
366        if (!$this->ID) {
367            return false;
368        }
369
370        $query_cols = $query_params = [];
371
372        foreach ((array) $data as $col => $value) {
373            $query_cols[]   = $this->db->quote_identifier($col) . ' = ?';
374            $query_params[] = $value;
375        }
376        $query_params[] = $iid;
377        $query_params[] = $this->ID;
378
379        $sql = "UPDATE ".$this->db->table_name('identities', true).
380            " SET `changed` = ".$this->db->now() . ", " . implode(', ', $query_cols)
381            . " WHERE `identity_id` = ?"
382                . " AND `user_id` = ?"
383                . " AND `del` <> 1";
384
385        $this->db->query($sql, $query_params);
386
387        // clear the cache
388        $this->identities = [];
389        $this->emails     = null;
390
391        return $this->db->affected_rows() > 0;
392    }
393
394    /**
395     * Create a new identity record linked with this user
396     *
397     * @param array $data Hash array with col->value pairs to save
398     *
399     * @return int|false The inserted identity ID or false on error
400     */
401    function insert_identity($data)
402    {
403        if (!$this->ID) {
404            return false;
405        }
406
407        unset($data['user_id']);
408
409        $insert_cols   = [];
410        $insert_values = [];
411
412        foreach ((array) $data as $col => $value) {
413            $insert_cols[]   = $this->db->quote_identifier($col);
414            $insert_values[] = $value;
415        }
416
417        $insert_cols[]   = $this->db->quote_identifier('user_id');
418        $insert_values[] = $this->ID;
419
420        $sql = "INSERT INTO " . $this->db->table_name('identities', true)
421            . " (`changed`, " . implode(', ', $insert_cols) . ")"
422            . " VALUES (" . $this->db->now() . ", " . implode(', ', array_pad([], count($insert_values), '?')) . ")";
423
424        $insert = $this->db->query($sql, $insert_values);
425
426        // clear the cache
427        $this->identities = [];
428        $this->emails     = null;
429
430        return $this->db->affected_rows($insert) ? $this->db->insert_id('identities') : false;
431    }
432
433    /**
434     * Mark the given identity as deleted
435     *
436     * @param int $iid Identity ID
437     *
438     * @return bool True if deleted successfully, false if nothing changed
439     */
440    function delete_identity($iid)
441    {
442        if (!$this->ID) {
443            return false;
444        }
445
446        $sql_result = $this->db->query(
447            "SELECT count(*) AS ident_count FROM " . $this->db->table_name('identities', true)
448            . " WHERE `user_id` = ? AND `del` <> 1",
449            $this->ID
450        );
451
452        $sql_arr = $this->db->fetch_assoc($sql_result);
453
454        // we'll not delete last identity
455        if ($sql_arr['ident_count'] <= 1) {
456            return false;
457        }
458
459        $this->db->query(
460            "UPDATE " . $this->db->table_name('identities', true)
461            . " SET `del` = 1, `changed` = " . $this->db->now()
462            . " WHERE `user_id` = ? AND `identity_id` = ?",
463            $this->ID,
464            $iid
465        );
466
467        // clear the cache
468        $this->identities = [];
469        $this->emails     = null;
470
471        return $this->db->affected_rows() > 0;
472    }
473
474    /**
475     * Make this identity the default one for this user
476     *
477     * @param int $iid The identity ID
478     */
479    function set_default($iid)
480    {
481        if ($this->ID && $iid) {
482            $this->db->query(
483                "UPDATE " . $this->db->table_name('identities', true)
484                . " SET `standard` = '0'"
485                . " WHERE `user_id` = ? AND `identity_id` <> ?",
486                $this->ID,
487                $iid
488            );
489
490            $this->db->query(
491                "UPDATE " . $this->db->table_name('identities', true)
492                . " SET `standard` = '1'"
493                . " WHERE `user_id` = ? AND `identity_id` = ?",
494                $this->ID,
495                $iid
496            );
497
498            $this->identities = [];
499        }
500    }
501
502    /**
503     * Update user's last_login timestamp
504     */
505    function touch()
506    {
507        if ($this->ID) {
508            $this->db->query(
509                "UPDATE " . $this->db->table_name('users', true)
510                . " SET `last_login` = " . $this->db->now()
511                . " WHERE `user_id` = ?",
512                $this->ID
513            );
514        }
515    }
516
517    /**
518     * Update user's failed_login timestamp and counter
519     */
520    function failed_login()
521    {
522        if ($this->ID && $this->rc->config->get('login_rate_limit', 3)) {
523            $counter = 0;
524
525            if (empty($this->data['failed_login'])) {
526                $failed_login = new DateTime('now');
527                $counter      = 1;
528            }
529            else {
530                $failed_login = new DateTime($this->data['failed_login']);
531                $threshold    = new DateTime('- 60 seconds');
532
533                if ($failed_login < $threshold) {
534                    $failed_login = new DateTime('now');
535                    $counter      = 1;
536                }
537            }
538
539            $this->db->query(
540                "UPDATE " . $this->db->table_name('users', true)
541                    . " SET `failed_login` = ?"
542                    . ", `failed_login_counter` = " . ($counter ?: "`failed_login_counter` + 1")
543                . " WHERE `user_id` = ?",
544                $failed_login, $this->ID
545            );
546        }
547    }
548
549    /**
550     * Checks if the account is locked, e.g. as a result of brute-force prevention
551     */
552    function is_locked()
553    {
554        if (empty($this->data['failed_login'])) {
555            return false;
556        }
557
558        if ($rate = (int) $this->rc->config->get('login_rate_limit', 3)) {
559            $last_failed = new DateTime($this->data['failed_login']);
560            $threshold   = new DateTime('- 60 seconds');
561
562            if ($last_failed > $threshold && $this->data['failed_login_counter'] >= $rate) {
563                return true;
564            }
565        }
566
567        return false;
568    }
569
570    /**
571     * Clear the saved object state
572     */
573    function reset()
574    {
575        $this->ID   = null;
576        $this->data = null;
577    }
578
579    /**
580     * Find a user record matching the given name and host
581     *
582     * @param string $user IMAP user name
583     * @param string $host IMAP host name
584     *
585     * @return rcube_user New user instance
586     */
587    static function query($user, $host)
588    {
589        $dbh    = rcube::get_instance()->get_dbh();
590        $config = rcube::get_instance()->config;
591
592        // query for matching user name
593        $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true)
594            ." WHERE `mail_host` = ? AND `username` = ?", $host, $user);
595
596        $sql_arr = $dbh->fetch_assoc($sql_result);
597
598        // username not found, try aliases from identities
599        if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) {
600            $sql_result = $dbh->limitquery("SELECT u.*"
601                . " FROM " . $dbh->table_name('users', true) . " u"
602                . " JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)"
603                . " WHERE `email` = ? AND `del` <> 1",
604                0, 1, $user
605            );
606
607            $sql_arr = $dbh->fetch_assoc($sql_result);
608        }
609
610        // user already registered -> overwrite username
611        if ($sql_arr) {
612            return new rcube_user($sql_arr['user_id'], $sql_arr);
613        }
614    }
615
616    /**
617     * Create a new user record and return a rcube_user instance
618     *
619     * @param string $user IMAP user name
620     * @param string $host IMAP host
621     *
622     * @return rcube_user|null New user instance on success, Null on error/abort
623     */
624    static function create($user, $host)
625    {
626        $user_name  = '';
627        $user_email = '';
628        $rcube      = rcube::get_instance();
629        $dbh        = $rcube->get_dbh();
630
631        // try to resolve user in virtuser table and file
632        if ($email_list = self::user2email($user, false, true)) {
633            $user_email = is_array($email_list[0]) ? $email_list[0]['email'] : $email_list[0];
634        }
635
636        $data = $rcube->plugins->exec_hook('user_create', [
637                'host'        => $host,
638                'user'        => $user,
639                'user_name'   => $user_name,
640                'user_email'  => $user_email,
641                'email_list'  => $email_list,
642                'language'    => isset($_SESSION['language']) ? $_SESSION['language'] : null,
643                'preferences' => [],
644        ]);
645
646        // plugin aborted this operation
647        if ($data['abort']) {
648            return;
649        }
650
651        $insert = $dbh->query(
652            "INSERT INTO " . $dbh->table_name('users', true)
653            . " (`created`, `last_login`, `username`, `mail_host`, `language`, `preferences`)"
654            . " VALUES (" . $dbh->now() . ", " . $dbh->now() . ", ?, ?, ?, ?)",
655            $data['user'],
656            $data['host'],
657            $data['language'],
658            serialize($data['preferences'])
659        );
660
661        if ($dbh->affected_rows($insert) && ($user_id = $dbh->insert_id('users'))) {
662            // create rcube_user instance to make plugin hooks work
663            $user_instance = new rcube_user($user_id, [
664                    'user_id'     => $user_id,
665                    'username'    => $data['user'],
666                    'mail_host'   => $data['host'],
667                    'language'    => $data['language'],
668                    'preferences' => serialize($data['preferences']),
669            ]);
670
671            $rcube->user = $user_instance;
672            $mail_domain = $rcube->config->mail_domain($data['host']);
673            $user_name   = $data['user_name'];
674            $user_email  = $data['user_email'];
675            $email_list  = $data['email_list'];
676
677            if (empty($email_list)) {
678                if (empty($user_email)) {
679                    $user_email = strpos($data['user'], '@') ? $user : sprintf('%s@%s', $data['user'], $mail_domain);
680                }
681                $email_list[] = $user_email;
682            }
683            // identities_level check
684            else if (count($email_list) > 1 && $rcube->config->get('identities_level', 0) > 1) {
685                $email_list = [$email_list[0]];
686            }
687
688            if (empty($user_name)) {
689                $user_name = $data['user'];
690            }
691
692            // create new identities records
693            $standard = 1;
694            foreach ($email_list as $row) {
695                $record = [];
696
697                if (is_array($row)) {
698                    if (empty($row['email'])) {
699                        continue;
700                    }
701                    $record = $row;
702                }
703                else {
704                    $record['email'] = $row;
705                }
706
707                if (empty($record['name'])) {
708                    $record['name'] = $user_name != $record['email'] ? $user_name : '';
709                }
710
711                $record['user_id']  = $user_id;
712                $record['standard'] = $standard;
713
714                $plugin = $rcube->plugins->exec_hook('identity_create',
715                    ['login' => true, 'record' => $record]);
716
717                if (!$plugin['abort'] && $plugin['record']['email']) {
718                    $rcube->user->insert_identity($plugin['record']);
719                }
720
721                $standard = 0;
722            }
723        }
724        else {
725            rcube::raise_error([
726                    'code' => 500, 'line' => __LINE__, 'file' => __FILE__,
727                    'message' => "Failed to create new user"
728                ],
729                true, false
730            );
731        }
732
733        return !empty($user_instance) ? $user_instance : null;
734    }
735
736    /**
737     * Resolve username using a virtuser plugins
738     *
739     * @param string $email E-mail address to resolve
740     *
741     * @return string Resolved IMAP username
742     */
743    static function email2user($email)
744    {
745        $rcube  = rcube::get_instance();
746        $plugin = $rcube->plugins->exec_hook('email2user', ['email' => $email, 'user' => null]);
747
748        return $plugin['user'];
749    }
750
751    /**
752     * Resolve e-mail address from virtuser plugins
753     *
754     * @param string $user     User name
755     * @param bool   $first    If true returns first found entry
756     * @param bool   $extended If true returns email as array (email and name for identity)
757     *
758     * @return mixed Resolved e-mail address string or array of strings
759     */
760    static function user2email($user, $first = true, $extended = false)
761    {
762        $rcube  = rcube::get_instance();
763        $plugin = $rcube->plugins->exec_hook('user2email', [
764                'email'    => null,
765                'user'     => $user,
766                'first'    => $first,
767                'extended' => $extended
768        ]);
769
770        return empty($plugin['email']) ? null : $plugin['email'];
771    }
772
773    /**
774     * Return a list of saved searches linked with this user
775     *
776     * @param int $type Search type
777     *
778     * @return array List of saved searches indexed by search ID
779     */
780    function list_searches($type)
781    {
782        $plugin = $this->rc->plugins->exec_hook('saved_search_list', ['type' => $type]);
783
784        if ($plugin['abort']) {
785            return (array) $plugin['result'];
786        }
787
788        $result = [];
789
790        $sql_result = $this->db->query(
791            "SELECT `search_id` AS id, `name`"
792            . " FROM " . $this->db->table_name('searches', true)
793            . " WHERE `user_id` = ? AND `type` = ?"
794            . " ORDER BY `name`",
795            (int) $this->ID, (int) $type
796        );
797
798        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
799            $result[$sql_arr['id']] = $sql_arr;
800        }
801
802        return $result;
803    }
804
805    /**
806     * Return saved search data.
807     *
808     * @param int $id Row identifier
809     *
810     * @return array Data
811     */
812    function get_search($id)
813    {
814        $plugin = $this->rc->plugins->exec_hook('saved_search_get', ['id' => $id]);
815
816        if ($plugin['abort']) {
817            return (array) $plugin['result'];
818        }
819
820        $sql_result = $this->db->query(
821            "SELECT `name`, `data`, `type`"
822            . " FROM ".$this->db->table_name('searches', true)
823            . " WHERE `user_id` = ? AND `search_id` = ?",
824            (int) $this->ID, (int) $id
825        );
826
827        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
828            return [
829                'id'   => $id,
830                'name' => $sql_arr['name'],
831                'type' => $sql_arr['type'],
832                'data' => unserialize($sql_arr['data']),
833            ];
834        }
835
836        return [];
837    }
838
839    /**
840     * Deletes given saved search record
841     *
842     * @param int $sid Search ID
843     *
844     * @return bool True if deleted successfully, false if nothing changed
845     */
846    function delete_search($sid)
847    {
848        if (!$this->ID) {
849            return false;
850        }
851
852        $this->db->query(
853            "DELETE FROM " . $this->db->table_name('searches', true)
854            ." WHERE `user_id` = ? AND `search_id` = ?",
855            (int) $this->ID, $sid
856        );
857
858        return $this->db->affected_rows() > 0;
859    }
860
861    /**
862     * Create a new saved search record linked with this user
863     *
864     * @param array $data Hash array with col->value pairs to save
865     *
866     * @return int The inserted search ID or false on error
867     */
868    function insert_search($data)
869    {
870        if (!$this->ID) {
871            return false;
872        }
873
874        $insert_cols[]   = 'user_id';
875        $insert_values[] = (int) $this->ID;
876        $insert_cols[]   = $this->db->quote_identifier('type');
877        $insert_values[] = (int) $data['type'];
878        $insert_cols[]   = $this->db->quote_identifier('name');
879        $insert_values[] = $data['name'];
880        $insert_cols[]   = $this->db->quote_identifier('data');
881        $insert_values[] = serialize($data['data']);
882
883        $sql = "INSERT INTO " . $this->db->table_name('searches', true)
884            . " (" . implode(', ', $insert_cols) . ")"
885            . " VALUES (" . implode(', ', array_pad([], count($insert_values), '?')) . ")";
886
887        $insert = $this->db->query($sql, $insert_values);
888
889        return $this->db->affected_rows($insert) ? $this->db->insert_id('searches') : false;
890    }
891}
892