root/dev/common/includes/class.db.mysqli.php @ 404

Revision 404, 25.4 KB (checked in by kovell, 11 years ago)

Fixes: db debug code no longer errors on some setups. upgrade now gives updates on progress. apicache returned to index.php for better eve api compatibility. cache options now has clear summary cache option. siglist shows correct urls. summary cache correctly updates. feedfetcher excludes kills before exclude date. exclude date on kills works

Line 
1<?php
2// mssql: select SCOPE_IDENTITY() AS id
3// postgresql: INSERT INTO mytable (lastname) VALUES ('Cher') RETURNING id;
4
5//! mysqli connection class.
6//! Establishes the connection to the database.
7class DBConnection_mysqli
8{
9    //! Set up a mysqli DB connection.
10    function DBConnection_mysqli()
11    {
12        static $conn_id;
13
14        if ($conn_id)
15        {
16            $this->id_ = $conn_id;
17            return;
18        }
19        if(defined('DB_PORT'))
20        {
21            if (!$this->id_ = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT))
22            die("Unable to connect to mysql database.");
23            $this->id_->set_charset('utf8');
24        }
25        else
26        {
27            if (!$this->id_ = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME))
28            die("Unable to connect to mysql database.");
29            $this->id_->set_charset('utf8');
30        }
31
32        //mysqli_select_db(DB_NAME);
33        $conn_id = $this->id_;
34    }
35    //! Return the connection id for this connection. Used for connection specific commands.
36    function id()
37    {
38        return $this->id_;
39    }
40    //! Return the number of rows affected by a query.
41    function affectedRows()
42    {
43        return mysqli_affected_rows($this->id_);
44    }
45}
46//! mysqli uncached query class. Manages SQL queries to a MySQL DB using mysqli.
47class DBNormalQuery_mysqli
48{
49    //! Prepare a connection for a new mysqli query.
50    function DBNormalQuery_mysqli()
51    {
52        $this->executed_ = false;
53        $this->dbconn_ = new DBConnection_mysqli;
54        static $totalexectime = 0;
55                $this->totalexectime_ = &$totalexectime;
56    }
57    //! Return the count of queries performed.
58
59    /*!
60     * \param $increase if true then increment the count.
61     * \return the count of queries so far.
62     */
63    function queryCount($increase = false)
64    {
65        static $count;
66
67        if ($increase)
68        {
69            $count++;
70        }
71
72        return $count;
73    }
74    //! Return the count of cached queries performed - 0 for uncache queries.
75    function queryCachedCount($increase = false)
76    {
77        return 0;
78    }
79    //! Execute an SQL string.
80
81    /*
82     * If DB_HALTONERROR is set then this will exit on an error.
83     * \return false on error or true if successful.
84     */
85    function execute($sql)
86    {
87        $t1 = strtok(microtime(), ' ') + strtok('');
88
89        $this->resid_ = mysqli_query($this->dbconn_->id(),$sql);
90
91        if ($this->resid_ === false || $this->dbconn_->id()->errno)
92        {
93            if(defined('KB_PROFILE'))
94                        {
95                                DBDebug::recordError("Database error: ".$this->dbconn_->id()->error);
96                                DBDebug::recordError("SQL: ".$sql);
97                        }
98            if (defined('DB_HALTONERROR') && DB_HALTONERROR)
99            {
100                echo "Database error: " . $this->dbconn_->id()->error . "<br>";
101                echo "SQL: " . $sql . "<br>";
102                exit;
103            }
104            else
105            {
106                return false;
107            }
108        }
109
110        $this->exectime_ = strtok(microtime(), ' ') + strtok('') - $t1;
111        $this->totalexectime_ += $this->exectime_;
112        $this->executed_ = true;
113
114        if(defined('KB_PROFILE')) DBDebug::profile($sql);
115
116        $this->queryCount(true);
117
118        return true;
119    }
120    //! Return the number of rows returned by the last query.
121    function recordCount()
122    {
123        if ($this->resid_)
124        {
125            return $this->resid_->num_rows;
126        }
127        return false;
128    }
129    //! Return the next row of results from the last query.
130    function getRow()
131    {
132        if ($this->resid_)
133        {
134            return $this->resid_->fetch_assoc();
135        }
136        return false;
137    }
138    //! Reset list of results to return the first row from the last query.
139    function rewind()
140    {
141        @mysqli_data_seek($this->resid_, 0);
142    }
143    //! Return the auto-increment ID from the last insert operation.
144    function getInsertID()
145    {
146        return $this->dbconn_->id()->insert_id;
147    }
148    //! Return the execution time of the last query.
149    function execTime()
150    {
151        return $this->exectime_;
152    }
153    //! Return true if a query has been executed or false if none has been.
154    function executed()
155    {
156        return $this->executed_;
157    }
158    //! Return the most recent error message for the DB connection.
159    function getErrorMsg()
160    {
161        $msg = $this->sql_ . "<br>";
162        $msg .= "Query failed. " . mysqli_error($this->dbconn_->id());
163
164        return $msg;
165    }
166    //! Set the autocommit status.
167
168    /*! The default of true commits after every query.
169     * If set to false the queries will not be commited until autocommit is set
170     * to true.
171     *  \param $commit The new autocommit status.
172     *  \return true on success and false on failure.
173     */
174    function autocommit($commit = true)
175    {
176        return $this->dbconn_->id()->autocommit($commit);
177    }
178    //! Rollback all queries in the current transaction.
179    function rollback()
180    {
181        return mysqli_rollback($this->dbconn_->id());
182    }
183}
184//! mysqli file-cached query class. Manages SQL queries to a MySQL DB using mysqli.
185class DBCachedQuery_mysqli
186{
187    //! Set up a mysqli cached query object with default values.
188    function DBCachedQuery_mysqli()
189    {
190        static $totalexectime = 0;
191                $this->totalexectime_ = &$totalexectime;
192        $this->executed_ = false;
193        $this->_cache = array();
194        $this->_cached = false;
195
196        // this is the minimum runtime a query has to run to be
197        // eligible for caching in seconds
198        $this->_minruntime = 0.05;
199
200        // maximum size of a cached result set (512kB)
201        $this->_maxcachesize = 524288;
202        $this->d = true;
203    }
204    //! Check if this query has been cached and the cache valid.
205
206    /*
207     * \return true if this query has been cached and the cache is valid.
208     */
209    function checkCache()
210    {
211        // only cache selects
212        // we don't use select ... into so there is no problem
213        $this->_sql = str_replace(array("\r\n", "\n"), ' ', $this->_sql);
214        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
215        {
216            // this is no select, update the table
217            $this->markAffectedTables();
218            return false;
219        }
220
221        if (file_exists(KB_CACHEDIR.'/qcache_qry_'.$this->_hash))
222        {
223            $this->_mtime = filemtime(KB_CACHEDIR.'/qcache_qry_'.$this->_hash);
224            /// Remove cached queries more than an hour old.
225            if (time() - $this->_mtime > 3600 )
226            {
227                unlink(KB_CACHEDIR.'/qcache_qry_'.$this->_hash);
228                return false;
229            }
230            if ($this->isCacheValid())
231            {
232                return true;
233            }
234        }
235
236        return false;
237    }
238
239    //! Extract all tables affected by a database modification.
240
241    //! The resulting list is set internally to this object.
242    function parseSQL()
243    {
244        // gets all involved tables for a select statement
245        $text = strtolower($this->_sql).' ';
246
247        // we try to get the text from 'from' to 'where' because all involved
248        // tables are declared in that part
249        $from = strpos($text, 'from')+5;
250        if (!$to = strpos($text, 'where'))
251        {
252            $to = strlen($text);
253        }
254        $parse = trim(substr($text, $from, $to-$from));
255
256        $tables = array();
257        if (strpos($parse, ',') !== false)
258        {
259            // , is a synonym for join so we'll replace them
260            $parse = str_replace(',', ' join ', $parse);
261        }
262
263        $parse = 'join '.$parse;
264        if (strpos($parse, 'join'))
265        {
266            // if this query is a join we parse it with regexp to get all tables
267            preg_match_all('/join (.*?) /', $parse, $match);
268            $tables = $match[1];
269        }
270        else
271        {
272            // no join so it is hopefully a simple table select
273            $tables[] = $parse;
274        }
275
276        $this->_usedtables = $tables;
277    }
278    //! Check if the cached query is valid.
279
280    /*! Determines whether the tables used by a query have been modified
281     * since the query was cached
282     */
283    function isCacheValid()
284    {
285        // check if cachefiles are still valid
286
287        // first, we need to get all involved tables
288        $this->parseSQL();
289
290        foreach ($this->_usedtables as $table)
291        {
292            $file = KB_CACHEDIR.'/qcache_tbl_'.trim($table);
293            if (file_exists($file))
294            {
295                // if one of the tables is outdated, the query is outdated
296                if ($this->_mtime < filemtime($file))
297                {
298                    return false;
299                }
300            }
301        }
302        return true;
303    }
304    //! Marks all tables affected by a database modification
305    function markAffectedTables()
306    {
307        // this function invalidates cache files for touched tables
308        $text = trim(strtolower($this->_sql));
309        $text = str_replace(array('ignore','`', "\r\n", "\n"), '', $text);
310        $text = str_replace('(', ' (', $text);
311        $ta = preg_split('/\s/', $text, 0, PREG_SPLIT_NO_EMPTY);
312
313        // check for sql keywords and get the table from the appropriate position
314        $tables = array();
315        if ($ta[0] == 'update')
316        {
317            $tables[] = $ta[1];
318        }
319        elseif ($ta[0] == 'insert')
320        {
321            $tables[] = $ta[2];
322        }
323        elseif ($ta[0] == 'replace')
324        {
325            $tables[] = $ta[2];
326        }
327        elseif ($ta[0] == 'delete')
328        {
329            $tables[] = $ta[2];
330        }elseif ($ta[0] == 'drop')
331        {
332            $tables[] = $ta[2];
333        }
334        elseif ($ta[0] == 'alter')
335        {
336            return false;
337        }
338        elseif ($ta[0] == 'create')
339        {
340            return false;
341        }
342        else
343        {
344            var_dump($ta);
345            trigger_error('No suitable handler for query found.',E_USER_WARNING);
346            return false;
347        }
348
349        foreach ($tables as $table)
350        {
351            $file = KB_CACHEDIR.'/qcache_tbl_'.$table;
352            @touch($file);
353        }
354        // refresh php's filestatcache so we dont get wrong timestamps on changed files
355        clearstatcache();
356    }
357    //! Generate the query cache.
358
359    //! Serialise a query and write to file.
360    function genCache()
361    {
362        // this function fetches all rows and writes the data into a textfile
363        // don't attemp to cache updates!
364        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
365        {
366            return false;
367        }
368
369        $bsize = 0;
370        while ($row = $this->getRow())
371        {
372            $this->_cache[] = $row;
373
374            // if the bytesize of the table exceeds the limit we'll abort
375            // the cache generation and leave this query unbuffered
376            $bsize += strlen(join('', $row));
377            if ($bsize > $this->_maxcachesize)
378            {
379                $this->_cache[] = array();
380                $this->_cached = false;
381                $this->rewind();
382                return false;
383            }
384        }
385
386        // write data into textfile
387        file_put_contents(KB_CACHEDIR.'/qcache_qry_'.$this->_hash, serialize($this->_cache));
388
389        $this->_cached = true;
390        $this->_currrow = 0;
391        $this->executed_ = true;
392    }
393    //! Read a cached query from file.
394    function loadCache()
395    {
396        // loads the cachefile into the memory
397        $this->_cache = unserialize(file_get_contents(KB_CACHEDIR.'/qcache_qry_'.$this->_hash));
398
399        $this->_cached = true;
400        $this->_currrow = 0;
401        $this->executed_ = true;
402    }
403
404    //! Execute an SQL string.
405
406    /*
407     * If DB_HALTONERROR is set then this will exit on an error.
408     * \return false on error or true if successful.
409     */
410    function execute($sql)
411    {
412        $this->_sql = trim($sql);
413        $this->_hash = md5($this->_sql);
414        $this->_cache = array();
415        $this->_cached = false;
416
417        if ($this->checkCache())
418        {
419            $this->loadCache();
420            $this->queryCachedCount(true);
421            return true;
422        }
423
424        // we got no or no valid cache so open the connection and run the query
425        $this->dbconn_ = new DBConnection_mysqli();
426
427        $t1 = strtok(microtime(), ' ') + strtok('');
428
429        $this->resid_ = mysqli_query($this->dbconn_->id(), $sql);
430
431        if (!$this->resid_ || $this->dbconn_->id()->errno)
432        {
433            if(defined('KB_PROFILE'))
434                        {
435                                DBDebug::recordError("Database error: ".$this->dbconn_->id()->error);
436                                DBDebug::recordError("SQL: ".$this->_sql);
437                        }
438            if (DB_HALTONERROR === true)
439            {
440                echo "Database error: ".$this->dbconn_->id()->error."<br/>";
441                echo "SQL: ".$this->_sql."<br/>";
442                exit;
443            }
444            else
445            {
446                return false;
447            }
448        }
449
450        $this->exectime_ = strtok(microtime(), ' ') + strtok('') - $t1;
451        $this->totalexectime_ += $this->exectime_;
452        $this->executed_ = true;
453
454        if(defined('KB_PROFILE')) DBDebug::profile($sql);
455
456        // if the query was too slow we'll fetch all rows and run it cached
457        if ($this->exectime_ > $this->_minruntime)
458        {
459            $this->genCache();
460        }
461
462        $this->queryCount(true);
463        return true;
464    }
465
466    //! Return the count of queries performed.
467
468    /*!
469     * \param $increase if true then increment the count.
470     * \return the count of queries so far.
471     */
472    function queryCount($increase = false)
473    {
474        static $count;
475
476        if ($increase)
477        {
478            $count++;
479        }
480
481        return $count;
482    }
483    //! Return the count of cached queries performed.
484
485    /*!
486     * \param $increase if true then increment the count.
487     * \return the count of queries so far.
488     */
489    function queryCachedCount($increase = false)
490    {
491        static $count;
492
493        if ($increase)
494        {
495            $count++;
496        }
497
498        return $count;
499    }
500
501    //! Return the number of rows returned by the last query.
502    function recordCount()
503    {
504        if ($this->_cached)
505        {
506            return count($this->_cache);
507        }
508        elseif ($this->resid_)
509        {
510            return $this->resid_->num_rows;
511        }
512        return false;
513    }
514
515    //! Return the next row of results from the last query.
516    function getRow()
517    {
518        if ($this->_cached)
519        {
520            if (!isset($this->_cache[$this->_currrow]))
521            {
522                return false;
523            }
524            // return the current row and increase the pointer by one
525            return $this->_cache[$this->_currrow++];
526        }
527        if ($this->resid_)
528        {
529            return $this->resid_->fetch_assoc();
530        }
531        return false;
532    }
533
534    //! Reset list of results to return the first row from the last query.
535    function rewind()
536    {
537        if ($this->_cached)
538        {
539            $this->_currrow = 0;
540        }
541                @mysqli_data_seek($this->resid_, 0);
542    }
543
544    //! Return the auto-increment ID from the last insert operation.
545    function getInsertID()
546    {
547        return $this->dbconn_->id()->insert_id;
548    }
549
550    //! Return the execution time of the last query.
551    function execTime()
552    {
553        return $this->exectime_;
554    }
555
556    //! Return true if a query has been executed or false if none has been.
557    function executed()
558    {
559        return $this->executed_;
560    }
561
562    //! Return the most recent error message for the DB connection.
563    function getErrorMsg()
564    {
565        $msg = $this->sql_."<br>";
566        $msg .= "Query failed. ".mysqli_error($this->dbconn_->id());
567
568        return $msg;
569    }
570
571    //! Set the autocommit status.
572
573    /*! The default of true commits after every query.
574     * If set to false the queries will not be commited until autocommit is set
575     * to true.
576     *  \param $commit The new autocommit status.
577     *  \return true on success and false on failure.
578     */
579    function autocommit($commit = true)
580    {
581        if(!$this->dbconn_) $this->dbconn_ = new DBConnection_mysqli();
582        return $this->dbconn_->id()->autocommit($commit);
583    }
584
585    //! Rollback all queries in the current transaction.
586    function rollback()
587    {
588        // if there's no connection to the db then there's nothing to roll back
589        if(!$this->dbconn_) return true;
590        return $this->dbconn_->id()->rollback();
591    }
592}
593
594//! mysqli memcached query class. Manages SQL queries to a MySQL DB using mysqli.
595class DBMemcachedQuery_mysqli
596{
597    function DBMemcachedQuery_mysqli()
598    {
599        static $totalexectime = 0;
600                $this->totalexectime_ = &$totalexectime;
601        $this->executed_ = false;
602        $this->_cache = array();
603        $this->_cached = false;
604
605        // this is the minimum runtime a query has to run to be
606        // eligible for caching in seconds
607        $this->_minruntime = 0.1;
608
609        // maximum size of a cached result set (1MB)
610        $this->_maxcachesize = 1048576;
611        $this->d = true;
612    }
613
614    //! Check if this query has been cached.
615
616    /*
617     * \return true if this query has been cached.
618     */
619    function checkCache()
620    {
621        global $mc;
622
623        // only cache selects
624        // we don't use select ... into so there is no problem
625        $this->_sql = str_replace(array("\r\n", "\n"), ' ', $this->_sql);
626        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
627        return false;
628
629        $cached = $mc->get(KB_SITE . '_sql_' . $this->_hash);
630        if($cached) {
631            return true;
632        }
633
634        return false;
635    }
636    //! Extract all tables affected by a database modification.
637
638    //! The resulting list is set internally to this object.
639    function parseSQL()
640    {
641        // gets all involved tables for a select statement
642        $text = strtolower($this->_sql).' ';
643
644        // we try to get the text from 'from' to 'where' because all involved
645        // tables are declared in that part
646        $from = strpos($text, 'from')+5;
647        if (!$to = strpos($text, 'where'))
648        {
649            $to = strlen($text);
650        }
651        $parse = trim(substr($text, $from, $to-$from));
652
653        $tables = array();
654        if (strpos($parse, ',') !== false)
655        {
656            // , is a synonym for join so we'll replace them
657            $parse = str_replace(',', ' join ', $parse);
658        }
659
660        $parse = 'join '.$parse;
661        if (strpos($parse, 'join'))
662        {
663            // if this query is a join we parse it with regexp to get all tables
664            preg_match_all('/join (.*?) /', $parse, $match);
665            $tables = $match[1];
666        }
667        else
668        {
669            // no join so it is hopefully a simple table select
670            $tables[] = $parse;
671        }
672
673        $this->_usedtables = $tables;
674    }
675
676    function genCache()
677    {
678        global $mc;
679
680        // this function fetches all rows and writes the data into a textfile
681
682        // don't attemp to cache updates!
683        if (strtolower(substr($this->_sql, 0, 6)) != 'select' && strtolower(substr($this->_sql, 0, 4)) != 'show')
684        {
685            return false;
686        }
687
688        $bsize = 0;
689        while ($row = $this->getRow())
690        {
691            $this->_cache[] = $row;
692
693            $bsize += strlen(join('', $row));
694            if ($bsize > $this->_maxcachesize)
695            {
696                $this->_cache[] = array();
697                $this->_cached = false;
698                $this->rewind();
699                return false;
700            }
701
702        }
703
704        // write data into textfile
705        $mc->set(KB_SITE . '_sql_' . $this->_hash, $this->_cache, 0, 600);
706
707        $this->_cached = true;
708        $this->_currrow = 0;
709        $this->executed_ = true;
710    }
711
712    //! Execute an SQL string.
713
714    /*
715     * If DB_HALTONERROR is set then this will exit on an error.
716     * \return false on error or true if successful.
717     */
718    function execute($sql)
719    {
720        global $mc;
721
722        $this->_sql = trim($sql);
723        $this->_hash = md5($this->_sql);
724        $this->_cache = array();
725        $this->_cached = false;
726
727        $cached = $mc->get(KB_SITE . '_sql_' . $this->_hash);
728        if($cached) {
729            $this->_cache = $cached;
730            $this->_cached = true;
731            $this->_currrow = 0;
732            $this->executed_ = true;
733            $this->queryCachedCount(true);
734            return true;
735        }
736
737        // we got no or no valid cache so open the connection and run the query
738        $this->dbconn_ = new DBConnection_mysqli;
739
740        $t1 = strtok(microtime(), ' ') + strtok('');
741
742        $this->resid_ = $this->dbconn_->id()->query($sql);
743
744        if (!$this->resid_ || $this->dbconn_->id()->errno)
745        {
746            if(defined('KB_PROFILE'))
747                        {
748                                DBDebug::recordError("Database error: ".$this->dbconn_->id()->error);
749                                DBDebug::recordError("SQL: ".$this->_sql);
750                        }
751            if (DB_HALTONERROR === true)
752            {
753                echo "Database error: ".$this->dbconn_->id()->error."<br/>";
754                echo "SQL: ".$this->_sql."<br/>";
755                exit;
756            }
757            else
758            {
759                return false;
760            }
761        }
762
763        $this->exectime_ = strtok(microtime(), ' ') + strtok('') - $t1;
764        $this->totalexectime_ += $this->exectime_;
765        $this->executed_ = true;
766
767        if(defined('KB_PROFILE')) DBDebug::profile($sql);
768
769        // if the query was too slow we'll fetch all rows and run it cached
770        $this->genCache();
771
772        $this->queryCount(true);
773        return true;
774    }
775
776    //! Return the count of queries performed.
777
778    /*!
779     * \param $increase if true then increment the count.
780     * \return the count of queries so far.
781     */
782    function queryCount($increase = false)
783    {
784        static $count;
785
786        if ($increase)
787        {
788            $count++;
789        }
790
791        return $count;
792    }
793
794    //! Return the count of cached queries performed.
795
796    /*!
797     * \param $increase if true then increment the count.
798     * \return the count of queries so far.
799     */
800    function queryCachedCount($increase = false)
801    {
802        static $count;
803
804        if ($increase)
805        {
806            $count++;
807        }
808
809        return $count;
810    }
811
812    //! Return the number of rows returned by the last query.
813    function recordCount()
814    {
815        if ($this->_cached)
816        {
817            return count($this->_cache);
818        }
819        elseif ($this->resid_)
820        {
821            return $this->resid_->num_rows;
822        }
823        return false;
824    }
825
826    //! Return the next row of results from the last query.
827    function getRow()
828    {
829        if ($this->_cached)
830        {
831            if (!isset($this->_cache[$this->_currrow]))
832            {
833                return false;
834            }
835            // return the current row and increase the pointer by one
836            return $this->_cache[$this->_currrow++];
837        }
838        if ($this->resid_)
839        {
840            return $this->resid_->fetch_assoc();
841        }
842        return false;
843    }
844
845    //! Reset list of results to return the first row from the last query.
846    function rewind()
847    {
848        if ($this->_cached)
849        {
850            $this->_currrow = 0;
851        }
852        @mysqli_data_seek($this->resid_, 0);
853    }
854
855    //! Return the auto-increment ID from the last insert operation.
856    function getInsertID()
857    {
858        return $this->dbconn_->id()->insert_id;
859    }
860
861    //! Return the execution time of the last query.
862    function execTime()
863    {
864        return $this->exectime_;
865    }
866
867    //! Return true if a query has been executed or false if none has been.
868    function executed()
869    {
870        return $this->executed_;
871    }
872
873    //! Return the most recent error message for the DB connection.
874    function getErrorMsg()
875    {
876        $msg = $this->sql_."<br>";
877        $msg .= "Query failed. ".mysqli_error($this->dbconn_->id());
878
879        return $msg;
880    }
881
882    //! Set the autocommit status.
883
884    /*! The default of true commits after every query.
885     * If set to false the queries will not be commited until autocommit is set
886     * to true.
887     *  \param $commit The new autocommit status.
888     *  \return true on success and false on failure.
889     */
890    function autocommit($commit = true)
891    {
892        if(!$this->dbconn_) $this->dbconn_ = new DBConnection_mysqli();
893        return $this->dbconn_->id()->autocommit($commit);
894    }
895
896    //! Rollback all queries in the current transaction.
897    function rollback()
898    {
899        // if there's no connection to the db then there's nothing to roll back
900        if(!$this->dbconn_) return true;
901        return $this->dbconn_->id()->rollback();
902    }
903}
904?>
Note: See TracBrowser for help on using the browser.