source: extensions/AMenuManager/amm_ajax.php @ 31815

Last change on this file since 31815 was 30623, checked in by plg, 9 years ago

bug fixed: makes sure we never return an array with a single empty value

  • Property svn:executable set to *
File size: 22.2 KB
Line 
1<?php
2/*
3 * -----------------------------------------------------------------------------
4 * Plugin Name: Advanced Menu Manager
5 * -----------------------------------------------------------------------------
6 * Author     : Grum
7 *   email    : grum@piwigo.org
8 *   website  : http://photos.grum.fr
9 *   PWG user : http://forum.piwigo.org/profile.php?id=3706
10 *
11 *   << May the Little SpaceFrog be with you ! >>
12 *
13 * -----------------------------------------------------------------------------
14 *
15 * See main.inc.php for release information
16 *
17 * manage all the ajax requests
18 * -----------------------------------------------------------------------------
19 */
20
21
22  define('PHPWG_ROOT_PATH',dirname(dirname(dirname(__FILE__))).'/');
23  if(!defined('AJAX_CALL')) define('AJAX_CALL', true);
24
25  /*
26   * set ajax module in admin mode if request is used for admin interface
27   */
28  if(!isset($_REQUEST['ajaxfct'])) $_REQUEST['ajaxfct']='';
29  if(preg_match('/^admin\./i', $_REQUEST['ajaxfct'])) define('IN_ADMIN', true);
30
31  // the common.inc.php file loads all the main.inc.php plugins files
32  include_once(PHPWG_ROOT_PATH.'include/common.inc.php' );
33  include_once(PHPWG_PLUGINS_PATH.'GrumPluginClasses/classes/GPCAjax.class.inc.php');
34  include_once('amm_root.class.inc.php');
35
36  load_language('plugin.lang', AMM_PATH);
37
38
39  class AMM_Ajax extends AMM_root
40  {
41    /**
42     * constructor
43     */
44    public function __construct($prefixeTable, $filelocation)
45    {
46      parent::__construct($prefixeTable, $filelocation);
47      $this->loadConfig();
48      $this->checkRequest();
49      $this->returnAjaxContent();
50    }
51
52    /**
53     * check the $_REQUEST values and set default values
54     *
55     */
56    protected function checkRequest()
57    {
58      global $user;
59
60      if(!isset($_REQUEST['errcode'])) $_REQUEST['errcode']='';
61      GPCAjax::checkToken();
62
63      // check if asked function is valid
64      if(!($_REQUEST[GPC_AJAX]=='admin.links.get' or
65           $_REQUEST[GPC_AJAX]=='admin.links.set' or
66           $_REQUEST[GPC_AJAX]=='admin.links.list' or
67           $_REQUEST[GPC_AJAX]=='admin.links.delete' or
68           $_REQUEST[GPC_AJAX]=='admin.links.order' or
69           $_REQUEST[GPC_AJAX]=='admin.links.setConfig' or
70           $_REQUEST[GPC_AJAX]=='admin.randomPict.setConfig' or
71           $_REQUEST[GPC_AJAX]=='admin.blocks.get' or
72           $_REQUEST[GPC_AJAX]=='admin.blocks.set' or
73           $_REQUEST[GPC_AJAX]=='admin.blocks.delete' or
74           $_REQUEST[GPC_AJAX]=='admin.blocks.list' or
75           $_REQUEST[GPC_AJAX]=='admin.album.setConfig' or
76           $_REQUEST[GPC_AJAX]=='admin.coreBlocks.setConfig'
77           )) $_REQUEST[GPC_AJAX]='';
78
79      if(preg_match('/^admin\./i', $_REQUEST[GPC_AJAX]) and !is_admin()) $_REQUEST[GPC_AJAX]='';
80
81
82      if($_REQUEST[GPC_AJAX]!='')
83      {
84        /*
85         * no check for admin.links.list request
86         */
87
88        /*
89         * check admin.links.get request
90         * check admin.blocks.get request
91         */
92        if($_REQUEST[GPC_AJAX]=='admin.links.get' or
93           $_REQUEST[GPC_AJAX]=='admin.blocks.get request')
94        {
95          if(!isset($_REQUEST['id'])) $_REQUEST['id']='';
96
97          if($_REQUEST['id']=='') $_REQUEST[GPC_AJAX]='';
98        }
99
100        /*
101         * check admin.links.set request
102         */
103        if($_REQUEST[GPC_AJAX]=='admin.links.set')
104        {
105          if(!isset($_REQUEST['id'])) $_REQUEST['id']='';
106          if(!isset($_REQUEST['datas']['label'])) $_REQUEST['datas']['label']='';
107          if(!isset($_REQUEST['datas']['url'])) $_REQUEST['datas']['url']='';
108          if(!isset($_REQUEST['datas']['icon'])) $_REQUEST['datas']['icon']='';
109          if(!isset($_REQUEST['datas']['mode'])) $_REQUEST['datas']['mode']='0';
110          if(!isset($_REQUEST['datas']['visible'])) $_REQUEST['datas']['visible']='n';
111          if(!isset($_REQUEST['datas']['accessUsers']) or $_REQUEST['datas']['accessUsers']=='') $_REQUEST['datas']['accessUsers']=array();
112          if(!isset($_REQUEST['datas']['accessGroups']) or $_REQUEST['datas']['accessGroups']=='') $_REQUEST['datas']['accessGroups']=array();
113
114          if($_REQUEST['datas']['label']=='' or
115             $_REQUEST['datas']['url']=='' or
116             $_REQUEST['datas']['icon']=='' or
117             !($_REQUEST['datas']['mode']=='0' or $_REQUEST['datas']['mode']=='1') or
118             !($_REQUEST['datas']['visible']=='y' or $_REQUEST['datas']['visible']=='n')
119            ) $_REQUEST[GPC_AJAX]='';
120        }
121
122        /*
123         * check admin.links.delete request
124         * check admin.blocks.delete request
125         *
126         */
127        if($_REQUEST[GPC_AJAX]=='admin.links.delete' or
128           $_REQUEST[GPC_AJAX]=='admin.blocks.delete')
129        {
130          if(!isset($_REQUEST['id'])) $_REQUEST['id']='';
131
132          if($_REQUEST['id']=='') $_REQUEST[GPC_AJAX]='';
133        }
134
135        /*
136         * check admin.links.order request
137         */
138        if($_REQUEST[GPC_AJAX]=='admin.links.order')
139        {
140          if(!isset($_REQUEST['datas']['links']) or $_REQUEST['datas']['links']=='') $_REQUEST['datas']['links']=array();
141
142          if(count($_REQUEST['datas']['links'])<=1) $_REQUEST[GPC_AJAX]='';
143        }
144
145
146        /*
147         * check admin.links.setConfig request
148         */
149        if($_REQUEST[GPC_AJAX]=='admin.links.setConfig')
150        {
151          if(!isset($_REQUEST['datas']['showIcons'])) $_REQUEST['datas']['showIcons']='';
152          if(!isset($_REQUEST['datas']['title']) or $_REQUEST['datas']['title']=='') $_REQUEST['datas']['title']=array();
153
154          if($_REQUEST['datas']['showIcons']=='' or
155             count($_REQUEST['datas']['title'])==0
156            ) $_REQUEST[GPC_AJAX]='';
157        }
158
159
160
161        /*
162         * check admin.randomPict.setConfig request
163         */
164        if($_REQUEST[GPC_AJAX]=='admin.randomPict.setConfig')
165        {
166          if(!isset($_REQUEST['datas']['blockHeight'])) $_REQUEST['datas']['blockHeight']='';
167          if(!isset($_REQUEST['datas']['blockTitles']) or $_REQUEST['datas']['blockTitles']=='') $_REQUEST['datas']['blockTitles']=array();
168          if(!isset($_REQUEST['datas']['infosName'])) $_REQUEST['datas']['infosName']='';
169          if(!isset($_REQUEST['datas']['infosComment'])) $_REQUEST['datas']['infosComment']='';
170          if(!isset($_REQUEST['datas']['freqDelay'])) $_REQUEST['datas']['freqDelay']='';
171          if(!isset($_REQUEST['datas']['selectMode'])) $_REQUEST['datas']['selectMode']='';
172          if(!isset($_REQUEST['datas']['selectCat']) or $_REQUEST['datas']['selectCat']=='') $_REQUEST['datas']['selectCat']=array();
173
174          if(!is_numeric($_REQUEST['datas']['blockHeight']) or
175             count($_REQUEST['datas']['blockTitles'])==0 or
176             !($_REQUEST['datas']['infosName']=='n' or
177               $_REQUEST['datas']['infosName']=='o' or
178               $_REQUEST['datas']['infosName']=='u') or
179             !($_REQUEST['datas']['infosComment']=='n' or
180               $_REQUEST['datas']['infosComment']=='o' or
181               $_REQUEST['datas']['infosComment']=='u') or
182             !is_numeric($_REQUEST['datas']['freqDelay']) or
183             !($_REQUEST['datas']['selectMode']=='a' or
184               $_REQUEST['datas']['selectMode']=='f' or
185               $_REQUEST['datas']['selectMode']=='c') or
186             ($_REQUEST['datas']['selectMode']=='c' and
187              count($_REQUEST['datas']['selectCat'])==0)
188            ) $_REQUEST[GPC_AJAX]='';
189        }
190
191
192        /*
193         * check admin.blocks.set request
194         */
195        if($_REQUEST[GPC_AJAX]=='admin.blocks.set')
196        {
197          if(!isset($_REQUEST['id'])) $_REQUEST['id']='';
198          if(!isset($_REQUEST['datas']['nfo'])) $_REQUEST['datas']['nfo']='';
199          if(!isset($_REQUEST['datas']['visible'])) $_REQUEST['datas']['visible']='';
200          if(!isset($_REQUEST['datas']['langs']) or $_REQUEST['datas']['langs']=='') $_REQUEST['datas']['langs']=array();
201
202          if($_REQUEST['datas']['nfo']=='' or
203             !($_REQUEST['datas']['visible']=='y' or $_REQUEST['datas']['visible']=='n') or
204             count($_REQUEST['datas']['langs'])==0
205            ) $_REQUEST[GPC_AJAX]='';
206        }
207
208
209
210
211        /*
212         * check admin.coreBlocks.setConfig request
213         */
214        if($_REQUEST[GPC_AJAX]=='admin.coreBlocks.setConfig')
215        {
216          if(!isset($_REQUEST['datas']['menuItems']) or $_REQUEST['datas']['menuItems']=='') $_REQUEST['datas']['menuItems']=array();
217          if(!isset($_REQUEST['datas']['blocks']) or $_REQUEST['datas']['blocks']=='') $_REQUEST['datas']['blocks']=array();
218
219          if(count($_REQUEST['datas']['menuItems'])!=count($this->defaultMenus)
220            ) $_REQUEST[GPC_AJAX]='';
221        }
222
223
224        /*
225         * check admin.album.setConfig request
226         */
227        if($_REQUEST[GPC_AJAX]=='admin.album.setConfig')
228        {
229          if(!isset($_REQUEST['datas']['selectCat']) or $_REQUEST['datas']['selectCat']=='') $_REQUEST['datas']['selectCat']=array();
230        }
231
232      }
233
234    } //checkRequest
235
236
237    /**
238     * return ajax content
239     */
240    protected function returnAjaxContent()
241    {
242      $result="KO!".l10n('g002_error_invalid_ajax_call');
243      switch($_REQUEST[GPC_AJAX])
244      {
245        case 'admin.links.get':
246          $result=$this->ajax_amm_admin_linksGet($_REQUEST['id']);
247          break;
248        case 'admin.links.set':
249          $result=$this->ajax_amm_admin_linksSet($_REQUEST['id'],$_REQUEST['datas']['label'],$_REQUEST['datas']['url'],$_REQUEST['datas']['mode'],$_REQUEST['datas']['icon'],$_REQUEST['datas']['visible'],$_REQUEST['datas']['accessUsers'],$_REQUEST['datas']['accessGroups']);
250          break;
251        case 'admin.links.list':
252          $result=$this->ajax_amm_admin_linksList();
253          break;
254        case 'admin.links.order':
255          $result=$this->ajax_amm_admin_linksOrder($_REQUEST['datas']['links']);
256          break;
257        case 'admin.links.delete':
258          $result=$this->ajax_amm_admin_linksDelete($_REQUEST['id']);
259          break;
260        case 'admin.links.setConfig':
261          $result=$this->ajax_amm_admin_linksSetConfig($_REQUEST['datas']);
262          break;
263
264        case 'admin.randomPict.setConfig':
265          $result=$this->ajax_amm_admin_randomPictSetConfig($_REQUEST['datas']);
266          break;
267
268        case 'admin.blocks.get':
269          $result=$this->ajax_amm_admin_blocksGet($_REQUEST['id']);
270          break;
271        case 'admin.blocks.set':
272          $result=$this->ajax_amm_admin_blocksSet($_REQUEST['id'],$_REQUEST['datas']['visible'],$_REQUEST['datas']['nfo'],$_REQUEST['datas']['langs']);
273          break;
274        case 'admin.blocks.list':
275          $result=$this->ajax_amm_admin_blocksList();
276          break;
277        case 'admin.blocks.delete':
278          $result=$this->ajax_amm_admin_blocksDelete($_REQUEST['id']);
279          break;
280
281        case 'admin.coreBlocks.setConfig':
282          $result=$this->ajax_amm_admin_coreBlocksSetConfig($_REQUEST['datas']['menuItems'], $_REQUEST['datas']['blocks']);
283          break;
284
285        case 'admin.album.setConfig':
286          $result=$this->ajax_amm_admin_albumSetConfig($_REQUEST['datas']);
287          break;
288      }
289      GPCAjax::returnResult($result);
290    }
291
292
293    /*
294     * -------------------------------------------------------------------------
295     *
296     * ADMIN FUNCTIONS
297     *
298     * -------------------------------------------------------------------------
299     */
300
301
302    /*
303     * -------------------------------------------------------------------------
304     * Links
305     * -------------------------------------------------------------------------
306     */
307
308    /**
309     * return a html formatted list of urls
310     */
311    private function ajax_amm_admin_linksList()
312    {
313      global $template, $user;
314      $local_tpl = new Template(AMM_PATH."admin/", "");
315      $local_tpl->set_filename('body_page',
316                    dirname($this->getFileLocation()).'/admin/amm_linkslinks_detail.tpl');
317
318
319
320      $datas['links']=array();
321
322      $links=$this->getLinks();
323      foreach($links as $link)
324      {
325        $datas['links'][]=array(
326          'id' => $link['id'],
327          'label' => $link['label'],
328          'url' => $link['url'],
329          'mode' => l10n("g002_mode_".$this->urlsModes[$link['mode']]),
330          'icon' => "plugins/".AMM_DIR."/links_pictures/".$link['icon'],
331          'visible' => l10n('g002_yesno_'.$link['visible'])
332        );
333      }
334      $local_tpl->assign('themeconf', $template->get_template_vars('themeconf'));
335      $local_tpl->assign('datas', $datas);
336      $local_tpl->assign('plugin', array('PATH' => AMM_PATH));
337
338      return($local_tpl->parse('body_page', true));
339    }
340
341
342    /**
343     * update links order
344     *
345     * @param Array $links
346     * @return String : OK or KO
347     */
348    private function ajax_amm_admin_linksOrder($links)
349    {
350      return($this->setLinksOrder($links)?'OK':'KO');
351    }
352
353
354    /**
355     * delete a link
356     *
357     * @param Integer $id : link id
358     * @return String : OK or KO
359     */
360    private function ajax_amm_admin_linksDelete($id)
361    {
362      return($this->deleteLink($id)?'OK':'KO');
363    }
364
365    /**
366     * return link content as a json string
367     *
368     * @param Integer $id : link id
369     * @return String : json string
370     */
371    private function ajax_amm_admin_linksGet($id)
372    {
373      $link=$this->getLink($id);
374     
375      if (empty($link['accessUsers']))
376      {
377        $link['accessUsers'] = array();
378      }
379      else
380      {
381        $link['accessUsers']=explode(',', $link['accessUsers']);
382      }
383
384      if (empty($link['accessGroups']))
385      {
386        $link['accessGroups'] = array();
387      }
388      else
389      {
390        $link['accessGroups']=explode(',', $link['accessGroups']);
391      }
392     
393      return(json_encode($link));
394    }
395
396    /**
397     * set link values
398     * if id is empty, create a new link
399     *
400     * @param String $id : link id
401     * @param String $label : link label
402     * @param String $url : link url
403     * @param String $mode : link mode (open a new window or not)
404     * @param String $icon : displayed icon
405     * @param String $visible : link visibility
406     * @return String : $id if OK, otherwise -1
407     */
408    private function ajax_amm_admin_linksSet($id, $label, $url, $mode, $icon, $visible, $accessUsers, $accessGroups)
409    {
410      return($this->setLink($id, $label, $url, $mode, $icon, $visible, implode(',', $accessUsers), implode(',', $accessGroups)));
411    }
412
413    /**
414     * set the links config
415     *
416     * $config is an array with keys :
417     *  String 'showIcons' : values 'y' or 'n'
418     *  Array  'titles'    : each array occurs is an array('id' => '', 'value' => '')
419     *                            id = lang id ('fr_FR', 'en_UK', ...)
420     *
421     * @param Array $config
422     * @return String : OK or KO
423     */
424    private function ajax_amm_admin_linksSetConfig($config)
425    {
426      $this->config['amm_links_show_icons']=$config['showIcons'];
427
428      $this->config['amm_links_title']=array();
429      foreach($config['title'] as $title)
430      {
431        $this->config['amm_links_title'][$title['id']]=base64_encode($title['value']);
432      }
433
434      $this->saveConfig();
435
436      return('OK!'.l10n('g002_config_saved'));
437    }
438
439
440    /*
441     * -------------------------------------------------------------------------
442     * random picture
443     * -------------------------------------------------------------------------
444     */
445
446
447    /**
448     * set the random picture config
449     *
450     * $config is an array with keys :
451     *  Integer 'blockHeight' : if value=0, the browser assume an automatic size
452     *  Array  'blockTitles'  : each array occurs is an array('id' => '', 'value' => '')
453     *                            id = lang id ('fr_FR', 'en_UK', ...)
454     *  String 'infosName'      : allow to display picture's name ;
455     *                              can take 'n' (no), 'o' (over), or 'u' (under)
456     *  String 'infosComment'   : allow to display picture's comment ;
457     *                              can take 'n' (no), 'o' (over), or 'u' (under)
458     *  Integer 'freqDelay'     : allow to choose change frequency (delay is in
459     *                            milliseconds)
460     *                            if value=0, there is no change
461     *  String 'selectMode'     : allows to choose picture to be randomly selected
462     *                              can take 'a' (all), 'f' (webmaster's favorites), 'c' (from categories)
463     *  Array  'selectCat'      : list of selected categories (choosing a cat implies to choose all sub cat)
464     *
465     * @param Array $config
466     * @return String : OK or KO
467     */
468    private function ajax_amm_admin_randomPictSetConfig($config)
469    {
470      $this->config['amm_randompicture_selectMode']=$config['selectMode'];
471      $this->config['amm_randompicture_selectCat']=$config['selectCat'];
472      $this->config['amm_randompicture_showname']=$config['infosName'];
473      $this->config['amm_randompicture_showcomment']=$config['infosComment'];
474      $this->config['amm_randompicture_periodicchange']=$config['freqDelay'];
475      $this->config['amm_randompicture_height']=($config['blockHeight']==99)?0:$config['blockHeight'];
476      $this->config['amm_randompicture_title']=array();
477      foreach($config['blockTitles'] as $title)
478      {
479        $this->config['amm_randompicture_title'][$title['id']]=base64_encode($title['value']);
480      }
481
482      $this->saveConfig();
483      return('OK!'.l10n('g002_config_saved'));
484    }
485
486
487
488    /*
489     * -------------------------------------------------------------------------
490     * personalised blocks
491     * -------------------------------------------------------------------------
492     */
493
494    /**
495     * return a html formatted list of blocks
496     */
497    private function ajax_amm_admin_blocksList()
498    {
499      global $template, $user;
500
501      $local_tpl = new Template(AMM_PATH."admin/", "");
502      $local_tpl->set_filename('body_page',
503                    dirname($this->getFileLocation()).'/admin/amm_personalised_detail.tpl');
504
505
506
507      $datas=array(
508        'blocks'=>array()
509      );
510
511      $blocks=$this->getPersonalisedBlocks(false, '', true);
512      foreach($blocks as $block)
513      {
514        $datas['blocks'][]=array(
515          'id' => $block['id'],
516          'nfo' => $block['nfo'],
517          'title' => $block['title'],
518          'visible' => l10n('g002_yesno_'.$block['visible'])
519        );
520      }
521
522      $local_tpl->assign('themeconf', $template->get_template_vars('themeconf'));
523      $local_tpl->assign('datas', $datas);
524      $local_tpl->assign('plugin', array('PATH' => AMM_PATH));
525
526      return($local_tpl->parse('body_page', true));
527    }
528
529    /**
530     * delete a block
531     *
532     * @param Integer $id : block id
533     * @return String : OK or KO
534     */
535    private function ajax_amm_admin_blocksDelete($id)
536    {
537      return($this->deletePersonalisedBlock($id)?'OK':'KO');
538    }
539
540    /**
541     * return block content as a json string
542     *
543     * @param Integer $id : block id
544     * @return String : json string
545     */
546    private function ajax_amm_admin_blocksGet($id)
547    {
548      return(json_encode($this->getPersonalisedBlock($id)));
549    }
550
551    /**
552     * set block values
553     * if id is empty, create a new block
554     *
555     * @param String $id      : block id
556     * @param String $visible : block visibility ('y' or 'n')
557     * @param String $nfo     : block description
558     * @param Array  $langs   : block langs, each record is an array
559     *                                  array('title' => '', 'content' => '')
560     * @return String : $id if OK, otherwise -1
561     */
562    private function ajax_amm_admin_blocksSet($id, $visible, $nfo, $lang)
563    {
564      return($this->setPersonalisedBlock($id, $visible, $nfo, $lang));
565    }
566
567    /*
568     * -------------------------------------------------------------------------
569     * core blocks
570     * -------------------------------------------------------------------------
571     */
572
573    /**
574     * set the menu config
575     *  - core blocks content
576     *  - blocks order&visibility
577     *
578     * @param Array $subMenus : for core blocks, sub menu items
579     *                          array(
580     *                            'subMenuId' => array(
581     *                                          'visibility' => (String),
582     *                                          'order' => (Integer),
583     *                                          'container' => (String),
584     *                                        )
585     *                          )
586     * @param Array $menus    : menu blocks order&visibility
587     *                          array(
588     *                            'block1' => array(
589     *                                'id'    => (String),
590     *                                'order' => (Integer),
591     *                                'users' => array(),
592     *                                'groups' => array()
593     *                            )
594     *                          )
595     * @return String : OK or KO
596     */
597    private function ajax_amm_admin_coreBlocksSetConfig($subMenu, $menus)
598    {
599      foreach($subMenu as $key=>$val)
600      {
601        if(!isset($subMenu[$key]['visibilityUser']) or $subMenu[$key]['visibilityUser']=='') $subMenu[$key]['visibilityUser']=array();
602        if(!isset($subMenu[$key]['visibilityGroup']) or $subMenu[$key]['visibilityGroup']=='') $subMenu[$key]['visibilityGroup']=array();
603
604        $subMenu[$key]['visibility']=implode(',', $subMenu[$key]['visibilityUser']).'/'.implode(',', $subMenu[$key]['visibilityGroup']);
605        unset($subMenu[$key]['visibilityUser']);
606        unset($subMenu[$key]['visibilityGroup']);
607      }
608      $this->config['amm_blocks_items']=$subMenu;
609      $this->saveConfig();
610
611
612      foreach($menus as $key=>$val)
613      {
614        if(!isset($menus[$key]['users']) or $menus[$key]['users']=='') $menus[$key]['users']=array();
615        if(!isset($menus[$key]['groups']) or $menus[$key]['groups']=='') $menus[$key]['groups']=array();
616      }
617      $this->setRegisteredBlocks($menus);
618
619      return('OK!'.l10n('g002_config_saved'));
620    }
621
622
623
624    /*
625     * -------------------------------------------------------------------------
626     * album to menu
627     * -------------------------------------------------------------------------
628     */
629
630
631    /**
632     * set the album to menu config
633     *
634     * $config is an array with keys :
635     *  Array  'selectCat'      : list of selected categories
636     *
637     * @param Array $config
638     * @return String : OK or KO
639     */
640    private function ajax_amm_admin_albumSetConfig($config)
641    {
642      $this->config['amm_albums_to_menu']=$config['selectCat'];
643
644      $this->saveConfig();
645      return('OK!'.l10n('g002_config_saved'));
646    }
647
648
649  } //class
650
651
652  $returned=new AMM_Ajax($prefixeTable, __FILE__);
653?>
654
Note: See TracBrowser for help on using the repository browser.