source: extensions/GrumPluginClasses/classes/GPCCore.class.inc.php @ 10246

Revision 10246, 22.3 KB checked in by grum, 9 years ago (diff)

bug:2149
Compatibility with Piwigo 2.2

Line 
1<?php
2
3/* -----------------------------------------------------------------------------
4  class name     : GPCCore
5  class version  : 1.4.0
6  plugin version : 3.5.0
7  date           : 2011-04-10
8  ------------------------------------------------------------------------------
9  author: grum at piwigo.org
10  << May the Little SpaceFrog be with you >>
11  ------------------------------------------------------------------------------
12
13  :: HISTORY
14
15| release | date       |
16| 1.0.0   | 2010/03/30 | * Update class & function names
17|         |            |
18| 1.1.0   | 2010/03/30 | * add the BBtoHTML function
19|         |            |
20| 1.2.0   | 2010/07/28 | * add the loadConfigFromFile function
21|         |            |
22| 1.3.0   | 2010/10/13 | * add the addHeaderCSS, addHeaderJS functions
23|         |            |
24| 1.3.1   | 2010/10/20 | * applyHeaderItems functions implemented with an
25|         |            |   higher priority on the 'loc_begin_page_header' event
26|         |            |
27|         |            | * implement the getUserLanguageDesc() function, using
28|         |            |   extended description function if present
29|         |            |
30|         |            | * implement the getPiwigoSystemPath() function
31|         |            |
32|         |            | * implement the rmDir() function
33|         |            |
34| 1.3.2   | 2011/01/28 | * implement the addUI() function
35|         |            |
36|         |            | * implement getMinified() & setMinifiedState() functions
37|         |            |
38| 1.3.3   | 2011/02/01 | * fix bug on loadConfig() function
39|         |            |
40|         |            | * update deleteConfig() function (allow to be used to
41|         |            |   delete the GPCCore config)
42|         |            |
43|         |            | * mantis bug:2167
44|         |            |
45| 1.3.4   | 2011/02/02 | * mantis bug:2170
46|         |            |   . File path for RBuilder registered plugins is corrupted
47|         |            |
48|         |            | * mantis bug:2178
49|         |            |   . RBuilder register function don't work
50|         |            |
51|         |            | * mantis bug:2179
52|         |            |   . JS file loaded in wrong order made incompatibility
53|         |            |     with Lightbox, GMaps & ASE plugins (and probably other)
54|         |            |
55| 1.4.0   | 2011/04/10 | * Updated for piwigo 2.2
56|         |            |
57|         |            |
58|         |            |
59
60  ------------------------------------------------------------------------------
61    no constructor, only static function are provided
62    - static function loadConfig
63    - static function loadConfigFromFile
64    - static function saveConfig
65    - static function deleteConfig
66    - static function getRegistered
67    - static function getModulesInfos
68    - static function register
69    - static function unregister
70    - static function BBtoHTML
71    - static function addHeaderCSS
72    - static function addHeaderJS
73    - static function addUI
74    - static function getMinified
75    - static function setMinifiedState
76    - static function getUserLanguageDesc
77    - static function getPiwigoSystemPath
78    - static function formatOctet
79    - static function rmDir
80   ---------------------------------------------------------------------- */
81
82
83
84class GPCCore
85{
86  static private $piwigoSystemPath;
87  static private $minified='.min';
88
89  static public $pluginName = "GPCCore";
90  static protected $headerItems = array(
91    'css' => array(),
92    'js'  => array()
93  );
94
95  static public function init()
96  {
97    self::$piwigoSystemPath=dirname(dirname(dirname(dirname(__FILE__))));
98  }
99
100  /* ---------------------------------------------------------------------------
101   * grum plugin classes informations functions
102   * -------------------------------------------------------------------------*/
103  static public function getModulesInfos()
104  {
105    return(
106      Array(
107        Array('name' => "CommonPlugin", 'version' => "2.2.0"),
108        Array('name' => "GPCAjax", 'version' => "3.0.0"),
109        Array('name' => "GPCCategorySelector", 'version' => "1.0.1"),
110        Array('name' => "GPCCore", 'version' => "1.4.0"),
111        Array('name' => "GPCCss", 'version' => "3.1.0"),
112        Array('name' => "GPCPagesNavigation", 'version' => "2.0.0"),
113        Array('name' => "GPCPublicIntegration", 'version' => "2.0.0"),
114        Array('name' => "GPCRequestBuilder", 'version' => "1.1.2"),
115        Array('name' => "GPCTables", 'version' => "1.5.0"),
116        Array('name' => "GPCTabSheet", 'version' => "1.1.1"),
117        Array('name' => "GPCTranslate", 'version' => "2.1.1"),
118        Array('name' => "GPCUsersGroups", 'version' => "2.1.0")
119      )
120    );
121  }
122
123
124  /* ---------------------------------------------------------------------------
125   * register oriented functions
126   * -------------------------------------------------------------------------*/
127
128  /**
129   * register a plugin using GPC
130   *
131   * @param String $pluginName : the plugin name
132   * @param String $release : the plugin version like "2.0.0"
133   * @param String $GPCNeed : the minimal version of GPC needed by the plugin to
134   *                          work
135   * @return Boolean : true if registering is Ok, otherwise false
136   */
137  static public function register($plugin, $release, $GPCneeded)
138  {
139    $config=Array();
140    if(!self::loadConfig(self::$pluginName, $config))
141    {
142      $config['registered']=array();
143    }
144
145    $config['registered'][$plugin]=Array(
146      'name' => $plugin,
147      'release' => $release,
148      'needed' => $GPCneeded,
149      'date' => date("Y-m-d"),
150    );
151    return(self::saveConfig(self::$pluginName, $config));
152  }
153
154  /**
155   * unregister a plugin using GPC
156   *
157   * assume that if the plugin was not registerd before, unregistering returns
158   * a true value
159   *
160   * @param String $pluginName : the plugin name
161   * @return Boolean : true if registering is Ok, otherwise false
162   */
163  static public function unregister($plugin)
164  {
165    $config=Array();
166    if(self::loadConfig(self::$pluginName, $config))
167    {
168      if(array_key_exists('registered', $config))
169      {
170        if(array_key_exists($plugin, $config['registered']))
171        {
172          unset($config['registered'][$plugin]);
173          return(self::saveConfig(self::$pluginName, $config));
174        }
175      }
176    }
177    // assume if the plugin was not registered before, unregistering it is OK
178    return(true);
179  }
180
181  /**
182   * @return Array : list of registered plugins
183   */
184  static public function getRegistered()
185  {
186    $config=Array();
187    if(self::loadConfig(self::$pluginName, $config))
188    {
189      if(array_key_exists('registered', $config))
190      {
191        return($config['registered']);
192      }
193    }
194    return(Array());
195  }
196
197
198
199  /* ---------------------------------------------------------------------------
200   * config oriented functions
201   * -------------------------------------------------------------------------*/
202
203  /**
204   *  load config from CONFIG_TABLE into an array
205   *
206   * @param String $pluginName : the plugin name, must contain only alphanumerical
207   *                             character
208   * @param Array $config : array, initialized or not with default values ; the
209   *                        config values are loaded in this value
210   * @return Boolean : true if config is loaded, otherwise false
211   */
212  static public function loadConfig($pluginName, &$config=Array())
213  {
214    global $conf;
215
216    if(!isset($conf[$pluginName.'_config']))
217    {
218      return(false);
219    }
220
221    $configValues = unserialize($conf[$pluginName.'_config']);
222    reset($configValues);
223    while (list($key, $val) = each($configValues))
224    {
225      if(is_array($val))
226      {
227        foreach($val as $key2 => $val2)
228        {
229          $config[$key][$key2]=$val2;
230        }
231      }
232      else
233      {
234        $config[$key] =$val;
235      }
236    }
237
238    $conf[$pluginName.'_config']=serialize($config);
239
240    return(true);
241  }
242
243  /**
244   *  load config from a file into an array
245   *
246   *  note : the config file is a PHP file one var $conf used as an array,
247   *  like the piwigo $conf var
248   *
249   * @param String $fileName : the file name
250   * @param Array $config : array, initialized or not with default values ; the
251   *                        config values are loaded in this value
252   * @return Boolean : true if config is loaded, otherwise false
253   */
254  static public function loadConfigFromFile($fileName, &$config=Array())
255  {
256    $conf=array();
257
258    if(!is_array($config) or !file_exists($fileName))
259    {
260      return(false);
261    }
262
263    include_once($fileName);
264
265    foreach($conf as $key=>$val)
266    {
267      $config[$key]=$val;
268    }
269    return(true);
270  }
271
272
273  /**
274   * save var $my_config into CONFIG_TABLE
275   *
276   * @param String $pluginName : the plugin name, must contain only alphanumerical
277   *                             character
278   * @param Array $config : array of configuration values
279   * @return Boolean : true if config is saved, otherwise false
280   */
281  static public function saveConfig($pluginName, $config)
282  {
283    global $conf;
284
285    $sql="REPLACE INTO ".CONFIG_TABLE."
286           VALUES('".$pluginName."_config', '"
287           .pwg_db_real_escape_string(serialize($config))."', '')";
288    $result=pwg_query($sql);
289    if($result)
290    {
291      $conf[$pluginName.'_config']=serialize($config);
292      return true;
293    }
294    else
295    {
296      return false;
297    }
298  }
299
300  /**
301   * delete config from CONFIG_TABLE
302   *
303   * @param String $pluginName : the plugin name, must contain only alphanumerical
304   *                             character ; if empty, assume GPCCore config
305   * @return Boolean : true if config is deleted, otherwise false
306   */
307  static public function deleteConfig($pluginName='')
308  {
309    if($pluginName=='') $pluginName=self::$pluginName;
310    $sql="DELETE FROM ".CONFIG_TABLE."
311          WHERE param='".$pluginName."_config'";
312    $result=pwg_query($sql);
313    if($result)
314    { return true; }
315    else
316    { return false; }
317  }
318
319
320  /**
321   * convert (light) BB tag to HTML tag
322   *
323   * all BB codes are not recognized, only :
324   *  - [ul] [/ul]
325   *  - [li] [/li]
326   *  - [b] [/b]
327   *  - [i] [/i]
328   *  - [url] [/url]
329   *  - carriage return is replaced by a <br>
330   *
331   * @param String $text : text to convert
332   * @return String : BB to HTML text
333   */
334  static public function BBtoHTML($text)
335  {
336    $patterns = Array(
337      '/\[li\](.*?)\[\/li\]\n*/im',
338      '/\[b\](.*?)\[\/b\]/ism',
339      '/\[i\](.*?)\[\/i\]/ism',
340      '/\[p\](.*?)\[\/p\]/ism',
341      '/\[url\]([\w]+?:\/\/[^ \"\n\r\t<]*?)\[\/url\]/ism',
342      '/\[url=([\w]+?:\/\/[^ \"\n\r\t<]*?)\](.*?)\[\/url\]/ism',
343      '/\n{0,1}\[ul\]\n{0,1}/im',
344      '/\n{0,1}\[\/ul\]\n{0,1}/im',
345      '/\n{0,1}\[ol\]\n{0,1}/im',
346      '/\n{0,1}\[\/ol\]\n{0,1}/im',
347      '/\n/im',
348    );
349    $replacements = Array(
350      '<li>\1</li>',
351      '<b>\1</b>',
352      '<i>\1</i>',
353      '<p>\1</p>',
354      '<a href="\1">\1</a>',
355      '<a href="\1">\2</a>',
356      '<ul>',
357      '</ul>',
358      '<ol>',
359      '</ol>',
360      '<br>',
361    );
362
363    return(preg_replace($patterns, $replacements, $text));
364  }
365
366  /**
367   * used to add a css file in the header
368   *
369   * @param String $id : a unique id for the file
370   * @param String $file : the css file
371   */
372  static public function addHeaderCSS($id, $file, $order=0)
373  {
374    global $template;
375
376    if(!array_key_exists($file, self::$headerItems['css']))
377    {
378      self::$headerItems['css'][$id]=$file;
379      $template->func_combine_css(array('path'=>$file, 'order'=>$order), $template->smarty);
380    }
381  }
382  static public function addHeaderJS($id, $file, $require=array())
383  {
384    global $template;
385
386    if(!array_key_exists($file, self::$headerItems['js']))
387    {
388      self::$headerItems['js'][$id]=$file;
389      $template->scriptLoader->add($id, 'header', $require, $file, 0);
390    }
391  }
392
393  /**
394   * add a ui component ; css & js dependencies are managed
395   *
396   * @param Array $list : possibles values are
397   *                        - inputCheckbox
398   *                        - inputColorPicker
399   *                        - inputColorsFB
400   *                        - inputConsole
401   *                        - inputDotArea
402   *                        - inputList
403   *                        - inputNum
404   *                        - inputPosition
405   *                        - inputRadio
406   *                        - inputStatusBar
407   *                        - inputText
408   *                        - categorySelector
409   */
410  static public function addUI($list)
411  {
412    global $template;
413
414    if(is_string($list)) $list=explode(',', $list);
415    if(!is_array($list)) return(false);
416
417    if(defined('IN_ADMIN'))
418    {
419      $themeFile=GPC_PATH.'css/%s_'.$template->get_themeconf('name').'.css';
420    }
421    else
422    {
423      $themeFile='themes/'.$template->get_themeconf('name').'/css/GPC%s.css';
424    }
425
426    foreach($list as $ui)
427    {
428      switch($ui)
429      {
430        case 'googleTranslate':
431          self::addHeaderJS('google.jsapi', 'http://www.google.com/jsapi');
432          self::addHeaderJS('gpc.googleTranslate', 'plugins/GrumPluginClasses/js/google_translate'.self::$minified.'.js', array('jquery', 'google.jsapi'));
433        case 'categorySelector':
434          self::addHeaderCSS('gpc.categorySelector', GPC_PATH.'css/categorySelector.css');
435          self::addHeaderCSS('gpc.categorySelectorT', sprintf($themeFile, 'categorySelector'));
436          self::addHeaderJS('gpc.categorySelector', GPC_PATH.'js/ui.categorySelector'.self::$minified.'.js', array('jquery'));
437          break;
438        case 'inputCheckbox':
439          self::addHeaderCSS('gpc.inputCheckbox', GPC_PATH.'css/inputCheckbox.css');
440          self::addHeaderJS('gpc.inputCheckbox', GPC_PATH.'js/ui.inputCheckbox'.self::$minified.'.js', array('jquery'));
441          break;
442        case 'inputColorPicker':
443          self::addHeaderCSS('gpc.inputText', GPC_PATH.'css/inputText.css');
444          self::addHeaderCSS('gpc.inputNum', GPC_PATH.'css/inputNum.css');
445          self::addHeaderCSS('gpc.inputColorsFB', GPC_PATH.'css/inputColorsFB.css');
446          self::addHeaderCSS('gpc.inputDotArea', GPC_PATH.'css/inputDotArea.css');
447          self::addHeaderCSS('gpc.inputColorPicker', GPC_PATH.'css/inputColorPicker.css');
448          self::addHeaderCSS('gpc.inputTextT', sprintf($themeFile, 'inputText'));
449          self::addHeaderCSS('gpc.inputNumT', sprintf($themeFile, 'inputNum'));
450          self::addHeaderCSS('gpc.inputColorsFBT', sprintf($themeFile, 'inputColorsFB'));
451          self::addHeaderCSS('gpc.inputDotAreaT', sprintf($themeFile, 'inputDotArea'));
452          self::addHeaderCSS('gpc.inputColorPickerT', sprintf($themeFile, 'inputColorPicker'));
453          self::addHeaderJS('jquery.ui', 'themes/default/js/ui/minified/jquery.ui.core.min.js', array('jquery'));
454          self::addHeaderJS('jquery.ui.slider', 'themes/default/js/ui/minified/jquery.ui.slider.min.js', array('jquery.ui'));
455          self::addHeaderJS('jquery.ui.draggable', 'themes/default/js/ui/minified/jquery.ui.draggable.min.js', array('jquery.ui'));
456          self::addHeaderJS('jquery.ui.dialog', 'themes/default/js/ui/minified/jquery.ui.slider.dialog.js', array('jquery.ui'));
457          self::addHeaderJS('gpc.inputText', GPC_PATH.'js/ui.inputText'.self::$minified.'.js', array('jquery'));
458          self::addHeaderJS('gpc.inputNum', GPC_PATH.'js/ui.inputNum'.self::$minified.'.js', array('jquery'));
459          self::addHeaderJS('gpc.inputColorsFB', GPC_PATH.'js/ui.inputColorsFB'.self::$minified.'.js', array('jquery'));
460          self::addHeaderJS('gpc.inputDotArea', GPC_PATH.'js/ui.inputDotArea'.self::$minified.'.js', array('jquery'));
461          self::addHeaderJS('gpc.inputColorPicker', GPC_PATH.'js/ui.inputColorPicker'.self::$minified.'.js', array('jquery.ui.slider','gpc.inputText','gpc.inputNum','gpc.inputColorsFB','gpc.inputDotArea'));
462          break;
463        case 'inputColorsFB':
464          self::addHeaderCSS('gpc.inputColorsFB', GPC_PATH.'css/inputColorsFB.css');
465          self::addHeaderCSS('gpc.inputColorsFBT', sprintf($themeFile, 'inputColorsFB'));
466          self::addHeaderJS('gpc.inputColorsFB', GPC_PATH.'js/ui.inputColorsFB'.self::$minified.'.js', array('jquery'));
467          break;
468        case 'inputConsole':
469          self::addHeaderCSS('gpc.inputConsole', GPC_PATH.'css/inputConsole.css');
470          self::addHeaderCSS('gpc.inputConsoleT', sprintf($themeFile, 'inputConsole'));
471          self::addHeaderJS('gpc.inputConsole', GPC_PATH.'js/ui.inputConsole'.self::$minified.'.js', array('jquery'));
472          break;
473        case 'inputDotArea':
474          self::addHeaderCSS('gpc.inputDotArea', GPC_PATH.'css/inputDotArea.css');
475          self::addHeaderCSS('gpc.inputDotAreaT', sprintf($themeFile, 'inputDotArea'));
476          self::addHeaderJS('gpc.inputDotArea', GPC_PATH.'js/ui.inputDotArea'.self::$minified.'.js', array('jquery'));
477          break;
478        case 'inputList':
479          self::addHeaderCSS('gpc.inputList', GPC_PATH.'css/inputList.css');
480          self::addHeaderCSS('gpc.inputListT', sprintf($themeFile, 'inputList'));
481          self::addHeaderJS('gpc.inputList', GPC_PATH.'js/ui.inputList'.self::$minified.'.js', array('jquery'));
482          break;
483        case 'inputNum':
484          self::addHeaderCSS('gpc.inputNum', GPC_PATH.'css/inputNum.css');
485          self::addHeaderCSS('gpc.inputNumT', sprintf($themeFile, 'inputNum'));
486          self::addHeaderJS('jquery.ui', 'themes/default/js/ui/minified/jquery.ui.core.min.js', array('jquery'));
487          self::addHeaderJS('jquery.ui.slider', 'themes/default/js/ui/minified/jquery.ui.slider.min.js', array('jquery.ui'));
488          self::addHeaderJS('gpc.inputNum', GPC_PATH.'js/ui.inputNum'.self::$minified.'.js', array('jquery','jquery.ui.slider'));
489          break;
490        case 'inputPosition':
491          self::addHeaderCSS('gpc.inputPosition', GPC_PATH.'css/inputPosition.css');
492          self::addHeaderCSS('gpc.inputPositionT', sprintf($themeFile, 'inputPosition'));
493          self::addHeaderJS('gpc.inputPosition', GPC_PATH.'js/ui.inputPosition'.self::$minified.'.js', array('jquery'));
494          break;
495        case 'inputRadio':
496          self::addHeaderJS('gpc.inputRadio', GPC_PATH.'js/ui.inputRadio'.self::$minified.'.js', array('jquery'));
497          break;
498        case 'inputStatusBar':
499          self::addHeaderCSS('gpc.inputStatusBar', GPC_PATH.'css/inputStatusBar.css');
500          self::addHeaderCSS('gpc.inputStatusBarT', sprintf($themeFile, 'inputStatusBar'));
501          self::addHeaderJS('gpc.inputStatusBar', GPC_PATH.'js/ui.inputStatusBar'.self::$minified.'.js', array('jquery'));
502          break;
503        case 'inputText':
504          self::addHeaderCSS('gpc.inputText', GPC_PATH.'css/inputText.css');
505          self::addHeaderCSS('gpc.inputTextT', sprintf($themeFile, 'inputText'));
506          self::addHeaderJS('gpc.inputText', GPC_PATH.'js/ui.inputText'.self::$minified.'.js', array('jquery'));
507          break;
508      }
509    }
510  }
511
512  /**
513   * return the minified value
514   *
515   * @return String
516   */
517  static public function getMinified()
518  {
519    return(self::$minified);
520  }
521
522  /**
523   * set the minified state
524   *
525   * @param Bool $state
526   * @return Bool
527   */
528  static public function setMinifiedState($state)
529  {
530    if($state)
531    {
532      self::$minified='.min';
533    }
534    else
535    {
536      self::$minified='';
537    }
538    return(self::$minified!='');
539  }
540
541
542  /**
543   * use the extended description get_user_language_desc() function if exist
544   * otherwise returns the value
545   *
546   * @param String $value : value to translate
547   * @return String : translated value
548   */
549  static public function getUserLanguageDesc($value)
550  {
551    if(function_exists('get_user_language_desc'))
552    {
553      return(get_user_language_desc($value));
554    }
555    else
556    {
557      return($value);
558    }
559  }
560
561
562  /**
563   * remove a path recursively
564   *
565   * @param String $directory : directory to remove
566   * @param Bool $removePath : if set to true, remove the path himself, if set
567   *                           to false, remove only file & sub-directories
568   * @return Bool : true if directory was succesfully removed, otherwise false
569   */
570  static public function rmDir($directory, $removePath=true)
571  {
572    $directory=rtrim($directory, '\/').'/';
573    $returned=true;
574    if(file_exists($directory) and is_dir($directory) and $directory!='./' and $directory!='../')
575    {
576      $dhandle=scandir($directory);
577      foreach($dhandle as $file)
578      {
579        if($file!='.' and $file!='..' )
580        {
581          if(is_dir($directory.$file))
582          {
583            $returned=self::rmDir($directory.$file, true) & $returned;
584          }
585          else
586          {
587            $returned=unlink($directory.$file) & $returned;
588          }
589        }
590      }
591      if($returned and $removePath) $returned=rmdir($directory);
592    }
593    return($returned);
594  }
595
596
597  /**
598   * returns the piwigo system path
599   * @return String
600   */
601  static public function getPiwigoSystemPath()
602  {
603    return(self::$piwigoSystemPath);
604  }
605
606
607 /**
608  * formats a file size into a human readable size
609  *
610  * @param String $format : "A"  : auto
611  *                         "Ai" : auto (io)
612  *                         "O"  : o
613  *                         "K"  : Ko
614  *                         "M"  : Mo
615  *                         "G"  : Go
616  *                         "Ki" : Kio
617  *                         "Mi" : Mio
618  *                         "Gi" : Gio
619  * @param String $thsep : thousand separator
620  * @param Integer $prec : number of decimals
621  * @param Bool $visible : display or not the unit
622  * @return String : a formatted file size
623  */
624 static public function formatOctet($octets, $format="Ai", $thsep="", $prec=2, $visible=true)
625 {
626  if($format=="Ai")
627  {
628   if($octets<1024)
629   { $format="O"; }
630   elseif($octets<1024000)
631   { $format="Ki"; }
632   elseif($octets<1024000000)
633   { $format="Mi"; }
634   else
635   { $format="Gi"; }
636  }
637  elseif($format=="A")
638  {
639   if($octets<1000)
640   { $format="O"; }
641   elseif($octets<1000000)
642   { $format="Ki"; }
643   elseif($octets<1000000000)
644   { $format="Mi"; }
645   else
646   { $format="Gi"; }
647  }
648
649  switch($format)
650  {
651   case "O":
652    $unit="o"; $div=1;
653    break;
654   case "K":
655    $unit="Ko"; $div=1000;
656    break;
657   case "M":
658    $unit="Mo"; $div=1000000;
659    break;
660   case "G":
661    $unit="Go"; $div=1000000000;
662    break;
663   case "Ki":
664    $unit="Kio"; $div=1024;
665    break;
666   case "Mi":
667    $unit="Mio"; $div=1024000;
668    break;
669   case "Gi":
670    $unit="Gio"; $div=1024000000;
671    break;
672  }
673
674  $returned=number_format($octets/$div, $prec, '.', $thsep);
675  if($visible) $returned.=' '.$unit;
676  return($returned);
677 } //function formatOctet
678
679
680} //class
681
682//add_event_handler('loc_begin_page_header', array('GPCCore', 'applyHeaderItems'), 10);
683
684GPCCore::init();
685
686?>
Note: See TracBrowser for help on using the repository browser.