4 // Copyright (C) 2005 Novell, Inc.
7 // Vijay K. Nanjundaswamy (knvijay@novell.com)
11 // Permission is hereby granted, free of charge, to any person obtaining a
12 // copy of this software and associated documentation files (the "Software"),
13 // to deal in the Software without restriction, including without limitation
14 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
15 // and/or sell copies of the Software, and to permit persons to whom the
16 // Software is furnished to do so, subject to the following conditions:
18 // The above copyright notice and this permission notice shall be included in
19 // all copies or substantial portions of the Software.
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27 // DEALINGS IN THE SOFTWARE.
31 using System
.Collections
;
32 using System
.Diagnostics
;
33 using System
.Threading
;
34 using System
.Reflection
;
37 using System
.Runtime
.Remoting
;
38 using System
.Runtime
.Remoting
.Channels
;
39 using System
.Runtime
.Remoting
.Channels
.Tcp
;
43 using BT
= Beagle
.Tile
;
46 namespace Beagle
.WebService
{
52 public string searchString
;
53 public string searchSource
;
54 public bool isLocalReq
;
55 public bool globalSearch
;
58 public class WebBackEnd
: MarshalByRefObject
{
60 static WebBackEnd instance
= null;
61 static Logger log
= Logger
.Get ("WebBackEnd");
63 private Hashtable result
;
64 private Hashtable sessionResp
;
67 result
= Hashtable
.Synchronized(new Hashtable());
68 sessionResp
= Hashtable
.Synchronized(new Hashtable());
76 public bool allowGlobalAccess
{
77 get { return WebServiceBackEnd.web_global; }
80 public string HostName
{
81 get { return WebServiceBackEnd.hostname; }
84 public static void init()
87 if (instance
== null) {
88 instance
= new WebBackEnd();
90 //Register TCP Channel Listener
91 ChannelServices
.RegisterChannel(new TcpChannel(8347));
93 WellKnownServiceTypeEntry WKSTE
=
94 new WellKnownServiceTypeEntry(typeof(WebBackEnd
),
95 "WebBackEnd.rem", WellKnownObjectMode
.Singleton
);
96 RemotingConfiguration
.ApplicationName
="beagled";
97 RemotingConfiguration
.RegisterWellKnownServiceType(WKSTE
);
101 void OnHitsAdded (QueryResult qres
, ICollection hits
)
103 if (result
.Contains(qres
)) {
105 Resp resp
= ((Resp
) result
[qres
]);
106 BT
.SimpleRootTile root
= resp
.resultPair
.rootTile
;
107 ArrayList hitsCopy
= resp
.resultPair
.hitsCopy
;
110 if (resp
.isLocalReq
) {
112 lock (hitsCopy
.SyncRoot
)
113 hitsCopy
.AddRange(hits
);
116 foreach (Hit h
in hits
)
117 if (h
.Uri
.ToString().StartsWith(NetworkedBeagle
.BeagleNetPrefix
) ||
118 WebServiceBackEnd
.AccessFilter
.FilterHit(h
)) {
120 lock (hitsCopy
.SyncRoot
)
128 void removeUris(ArrayList res
, ICollection uris
)
130 foreach(Uri u
in uris
)
131 foreach(Hit h
in res
)
132 if (h
.Uri
.Equals (u
) && h
.Uri
.Fragment
== u
.Fragment
) {
133 lock (res
.SyncRoot
) {
140 void OnHitsSubtracted (QueryResult qres
, ICollection uris
)
142 if (result
.Contains(qres
)) {
143 BT
.SimpleRootTile root
= ((Resp
) result
[qres
]).resultPair
.rootTile
;
145 root
.Subtract (uris
);
146 removeUris(((Resp
) result
[qres
]).resultPair
.hitsCopy
, uris
);
151 void OnFinished (QueryResult qres
)
153 if (result
.Contains(qres
))
154 log
.Info("WebBackEnd:OnFinished() - Got {0} results from beagled QueryDriver", ((Resp
) result
[qres
]).resultPair
.rootTile
.HitCollection
.NumResults
);
156 DetachQueryResult(qres
);
159 void OnCancelled (QueryResult qres
)
161 DetachQueryResult(qres
);
164 private void AttachQueryResult (QueryResult qres
, Resp resp
)
168 qres
.HitsAddedEvent
+= OnHitsAdded
;
169 qres
.HitsSubtractedEvent
+= OnHitsSubtracted
;
170 qres
.FinishedEvent
+= OnFinished
;
171 qres
.CancelledEvent
+= OnCancelled
;
173 result
.Add(qres
, resp
);
177 private void DetachQueryResult (QueryResult qres
)
181 if (result
.Contains(qres
))
183 Resp resp
= ((Resp
) result
[qres
]);
184 ArrayList hitsCopy
= resp
.resultPair
.hitsCopy
;
185 if (hitsCopy
!= null)
188 resp
.bufferContext
.maxDisplayed
= 0;
193 qres
.HitsAddedEvent
-= OnHitsAdded
;
194 qres
.HitsSubtractedEvent
-= OnHitsSubtracted
;
195 qres
.FinishedEvent
-= OnFinished
;
196 qres
.CancelledEvent
-= OnCancelled
;
202 const string NO_RESULTS
= "No results.";
204 private string getResultsLabel(BT
.SimpleRootTile root
)
207 if (root
.HitCollection
.NumResults
== 0)
209 else if (root
.HitCollection
.FirstDisplayed
== 0)
210 label
= String
.Format ("<b>{0} results of {1}</b> are shown.",
211 root
.HitCollection
.LastDisplayed
+ 1,
212 root
.HitCollection
.NumResults
);
214 label
= String
.Format ("Results <b>{0} through {1} of {2}</b> are shown.",
215 root
.HitCollection
.FirstDisplayed
+ 1,
216 root
.HitCollection
.LastDisplayed
+ 1,
217 root
.HitCollection
.NumResults
);
221 public bool canForward(string sessId
)
223 Resp resp
= (Resp
) sessionResp
[sessId
];
227 BT
.SimpleRootTile root
= resp
.resultPair
.rootTile
;
228 return (root
!= null)? root
.HitCollection
.CanPageForward
:false;
231 public string doForward(string sessId
)
233 Resp resp
= (Resp
) sessionResp
[sessId
];
235 if (!canForward(sessId
) || (resp
== null))
238 BT
.SimpleRootTile root
= resp
.resultPair
.rootTile
;
241 root
.HitCollection
.PageForward ();
243 bufferRenderContext bctx
= resp
.bufferContext
;
246 return (getResultsLabel(root
) + (resp
.isLocalReq
? bctx
.buffer
:bctx
.bufferForExternalQuery
));
253 public bool canBack(string sessId
)
255 Resp resp
= (Resp
) sessionResp
[sessId
];
259 BT
.SimpleRootTile root
= resp
.resultPair
.rootTile
;
260 return (root
!= null) ? root
.HitCollection
.CanPageBack
:false;
263 public string doBack(string sessId
)
265 Resp resp
= (Resp
) sessionResp
[sessId
];
266 if (!canBack(sessId
) || (resp
== null))
269 BT
.SimpleRootTile root
= resp
.resultPair
.rootTile
;
273 root
.HitCollection
.PageBack();
275 bufferRenderContext bctx
= resp
.bufferContext
;
278 return (getResultsLabel(root
) + (resp
.isLocalReq
? bctx
.buffer
:bctx
.bufferForExternalQuery
));
285 public bool NetworkBeagleActive
287 get {return NetworkedBeagle.NetBeagleListActive;}
290 public string doQuery(webArgs wargs
)
292 if (wargs
.sessId
== null || wargs
.searchString
== null || wargs
.searchString
== "")
295 log
.Debug("WebBackEnd: Got Search String: " + wargs
.searchString
);
297 Query query
= new Query();
298 query
.AddText (wargs
.searchString
);
299 if (wargs
.searchSource
!= null && wargs
.searchSource
!= "")
301 query
.AddSource(wargs
.searchSource
);
302 query
.AddDomain(QueryDomain
.System
);
305 query
.AddDomain (wargs
.globalSearch
? QueryDomain
.Global
:QueryDomain
.System
);
307 QueryResult qres
= new QueryResult ();
309 //Note: QueryDriver.DoQuery() local invocation is used.
310 //The root tile is used only for adding hits and generating html.
311 BT
.SimpleRootTile root
= new BT
.SimpleRootTile ();
313 //root.SetSource (searchSource); Do not SetSource on root!
315 ResultPair rp
= new ResultPair(root
);
316 bufferRenderContext bctx
= new bufferRenderContext(rp
);
317 Resp resp
= new Resp(rp
, bctx
, wargs
.isLocalReq
);
319 AttachQueryResult (qres
, resp
);
321 //Add sessionId-Resp mapping
322 if (sessionResp
.Contains(wargs
.sessId
))
323 sessionResp
[wargs
.sessId
] = resp
;
325 sessionResp
.Add(wargs
.sessId
, resp
);
327 log
.Info("WebBackEnd: Starting Query for string \"{0}\"", query
.QuotedText
);
329 QueryDriver
.DoQuery (query
, qres
);
331 //Wait only till we have enough results to display
332 while ((result
.Contains(qres
)) &&
333 (root
.HitCollection
.NumResults
< 10))
336 if (root
.HitCollection
.IsEmpty
)
341 return (getResultsLabel(root
) + (wargs
.isLocalReq
? bctx
.buffer
:bctx
.bufferForExternalQuery
));
345 public void dispatchAction (string sessId
, string actionString
)
347 string tile_id
= null, action
= null;
348 bool actionDone
= false;
350 //if (actionString.StartsWith ("dynaction:")) {
352 bufferRenderContext b
= ((Resp
)sessionResp
[sessId
]).bufferContext
;
354 actionDone
= b
.DoAction(actionString
);
360 if (actionString
.StartsWith ("action:")) {
362 int pos1
= "action:".Length
;
363 int pos2
= actionString
.IndexOf ("!");
368 tile_id
= actionString
.Substring (pos1
, pos2
- pos1
);
369 action
= actionString
.Substring (pos2
+ 1);
371 log
.Debug("WebBackEnd tile_id: {0}, action: {1}", tile_id
, action
);
373 BT
.Tile t
= ((Resp
)sessionResp
[sessId
]).GetTile (tile_id
);
378 MethodInfo info
= t
.GetType().GetMethod (action
,
379 BindingFlags
.Public
| BindingFlags
.NonPublic
|
380 BindingFlags
.Instance
, null,
381 CallingConventions
.Any
, new Type
[] {}, null);
384 log
.Warn ("WebBackEnd:dispatchAction couldn't find method called {0}", action
);
388 object[] attrs
= info
.GetCustomAttributes (false);
389 foreach (object attr
in attrs
) {
390 if (attr
is BT
.TileActionAttribute
) {
391 info
.Invoke (t
, null);
395 log
.Warn ("WebBackEnd:dispatchAction {0} does not have the TileAction attribute", t
);
398 string command
= null;
399 string commandArgs
= null;
401 if (actionString
.StartsWith ("http://") || actionString
.StartsWith ("file://")) {
402 command
= "gnome-open";
403 commandArgs
= "'" + actionString
+ "'";
405 else if (actionString
.StartsWith ("mailto:")) {
406 command
= "evolution";
407 commandArgs
= actionString
;
410 if (command
!= null) {
411 Process p
= new Process ();
412 p
.StartInfo
.UseShellExecute
= false;
413 p
.StartInfo
.FileName
= command
;
414 if (commandArgs
!= null)
416 p
.StartInfo
.Arguments
= commandArgs
;
425 //////////////////////////////////////////////////////////////////////////
427 private class ResultPair
{
428 private BT
.SimpleRootTile _rootTile
;
429 private ArrayList _hitsCopy
;
431 public ResultPair(BT
.SimpleRootTile rootTile
) {
432 this._rootTile
= rootTile
;
433 _hitsCopy
= ArrayList
.Synchronized(new ArrayList());
436 public BT
.SimpleRootTile rootTile
{
437 get { return _rootTile; }
440 public ArrayList hitsCopy
{
441 get { return _hitsCopy; }
447 private ResultPair _rp
;
448 private bufferRenderContext bufCtx
= null;
449 private bool _localRequest
;
451 private Hashtable tileTab
= null;
453 public Resp(ResultPair rp
, bufferRenderContext bCtx
, bool isLocalReq
)
457 this._localRequest
= isLocalReq
;
459 this.tileTab
= bCtx
.table
;
462 public ResultPair resultPair
{
465 public bufferRenderContext bufferContext
{
466 get { return bufCtx; }
468 public bool isLocalReq
{
469 get { return _localRequest; }
472 public BT
.Tile
GetTile (string key
)
475 return resultPair
.rootTile
;
477 return (Beagle
.Tile
.Tile
) tileTab
[key
];
481 //////////////////////////////////////////////////////////////////////////
482 private class bufferRenderContext
: BT
.TileRenderContext
{
484 private ResultPair _rp
;
485 private Hashtable tileTable
= null;
486 private Hashtable actionTable
= null;
489 private System
.Text
.StringBuilder sb
;
490 private bool renderStylesDone
= false;
492 public bufferRenderContext (ResultPair rp
)
495 this.tileTable
= Hashtable
.Synchronized(new Hashtable());
496 this.actionTable
= new Hashtable ();
500 public string buffer
{
501 get { return sb.ToString(); }
504 public Hashtable table
{
505 get { return tileTable; }
508 public string bufferForExternalQuery
{
511 //Substitute "action:_tile_id!Open" with "http://host:port/beagle?xxxx"
513 string[] list
= sb
.ToString().Split('\"');
514 for (int i
= 0; i
< list
.Length
; i
++) {
517 if (s
.StartsWith("action") && s
.EndsWith("!Open")) {
519 string[] s1
= s
.Split(':');
521 string[] s2
= s1
[1].Split('!');
523 BT
.Tile t
= (BT
.Tile
) table
[s2
[0]];
524 list
[i
] = WebServiceBackEnd
.AccessFilter
.TranslateHit(t
.Hit
);
525 t
.Uri
= new Uri(list
[i
]);
530 return String
.Join ("\"", list
);
537 sb
= new StringBuilder(4096);
538 renderStylesDone
= false;
541 tileTable
[_rp
.rootTile
.UniqueKey
] = _rp
.rootTile
;
544 /////////////////////////////////////////////////
545 public void ClearActions ()
551 private string AddAction (BT
.TileActionHandler handler
)
554 return "dynaction:NULL";
555 string key
= "dynaction:" + actionId
.ToString ();
557 actionTable
[key
] = handler
;
561 public bool DoAction (string key
)
563 BT
.TileActionHandler handler
= (BT
.TileActionHandler
) actionTable
[key
];
564 if (handler
!= null) {
570 /////////////////////////////////////////////////
572 override public void Write (string markup
)
577 override public void Link (string label
,
578 BT
.TileActionHandler handler
)
580 string key
= AddAction (handler
);
581 Write ("<a href=\"{0}\">{1}</a>", key
, label
);
584 override public void Tile (BT
.Tile tile
)
586 tileTable
[tile
.UniqueKey
] = tile
;
588 if (!renderStylesDone
) {
589 //KNV: Using static_stylesheet for now. Replace with TileCanvas logic later:
590 Write(static_stylesheet
);
592 Write ("<style type=\"text/css\" media=\"screen\">");
593 TileCanvas.RenderStyles (this);
596 renderStylesDone
= true;
601 if (tile
is BT
.TileHitCollection
)
602 PrefetchSnippetsForNetworkHits((BT
.TileHitCollection
)tile
);
607 /////////////////////////////////////////////////
608 // Code to scan forward through result set & prefetch/cache Snippets for Network Hits
610 public int maxDisplayed
= 0;
611 const int MAX_HIT_IDS_PER_REQ
= 20; //Max no. of hits snippets to seek at a time
612 const int MAX_HITS_AHEAD
= 40; //No. of hits ahead of lastDisplayed to scan
614 private bool tenHits
= false; //Flag to do Prefetch check only every 10 hits
616 private void PrefetchSnippetsForNetworkHits(BT
.TileHitCollection thc
)
618 int lastDisplayed
= 0;
620 if (maxDisplayed
!= 0)
621 lastDisplayed
= thc
.LastDisplayed
+ 1;
623 //We have cached snippets for network hits upto maxDisplayed
624 if (lastDisplayed
< maxDisplayed
)
627 maxDisplayed
= thc
.LastDisplayed
+ 1;
629 //Do Prefetch check once every ten hits
634 if (lastDisplayed
< thc
.NumResults
) {
637 ArrayList networkHits
= new ArrayList();
639 if ((thc
.NumResults
- lastDisplayed
) > MAX_HITS_AHEAD
)
640 limit
= lastDisplayed
+ MAX_HITS_AHEAD
;
642 limit
= thc
.NumResults
;
644 ArrayList hits
= _rp
.hitsCopy
;
645 lock (hits
.SyncRoot
) {
647 if (limit
> hits
.Count
)
650 log
.Debug("PrefetchSnippets: Scanning result set for Network Hits from {0} to {1}", lastDisplayed
, limit
);
652 //Get all NetworkHits with snippets field not initialized:
653 for (int si
= lastDisplayed
; si
< limit
; si
++)
655 if ((hits
[si
] is NetworkHit
) && (((NetworkHit
)hits
[si
]).snippet
== null))
656 networkHits
.Add((NetworkHit
)hits
[si
]);
660 log
.Debug("PrefetchSnippets: Found {0} NetworkHits without snippets", networkHits
.Count
);
662 while (networkHits
.Count
> 0) {
664 ArrayList nwHitsPerNode
= new ArrayList();
665 string hostnamePort
= GetHostnamePort((NetworkHit
)networkHits
[0]);
667 //Gather NetworkHits from a specific target Networked Beagle
668 foreach (NetworkHit nh
in networkHits
)
670 string hnp
= GetHostnamePort(nh
);
674 if (hnp
.Equals(hostnamePort
)) {
676 if (nwHitsPerNode
.Count
< MAX_HIT_IDS_PER_REQ
)
677 nwHitsPerNode
.Add(nh
);
683 //Remove NetworkHits for this Networked Beagle
684 int i
= networkHits
.Count
;
687 string hnp
= GetHostnamePort((NetworkHit
)networkHits
[i
]);
688 if ((hnp
== null) || hnp
.Equals(hostnamePort
))
689 networkHits
.RemoveAt(i
);
692 if (nwHitsPerNode
.Count
> 0)
694 string[] f3
= hostnamePort
.Split(':');
697 log
.Warn("PrefetchSnippets: Invalid format netBeagle URI in NetworkHit");
700 BeagleWebService wsp
= new BeagleWebService(f3
[0], f3
[1]);
702 string searchToken
= GetSearchToken((NetworkHit
)nwHitsPerNode
[0]);
704 if (searchToken
.Equals("beagle")) //Check if it is Older version of Beagle networking
707 if (searchToken
!= null) {
709 int[] hitIds
= new int[nwHitsPerNode
.Count
];
710 for (int j
= 0; j
< hitIds
.Length
; j
++)
711 hitIds
[j
] = ((NetworkHit
)nwHitsPerNode
[j
]).Id
;
713 log
.Debug("PrefetchSnippets: Invoking GetSnippets on {0} for {1} hits", wsp
.Hostname
, nwHitsPerNode
.Count
);
715 ReqContext2 rc
= new ReqContext2(wsp
, nwHitsPerNode
, thc
);
716 wsp
.BeginGetSnippets(searchToken
, hitIds
, PrefetchSnippetsResponseHandler
, rc
);
719 //Signal change in TileHitCollection due to addition of snippets:
720 //_rp.rootTile.HitCollection.ClearSources(null);
726 private static void PrefetchSnippetsResponseHandler(IAsyncResult ar
)
728 ReqContext2 rc
= (ReqContext2
)ar
.AsyncState
;
730 ArrayList nwHits
= rc
.GetNwHits
;
731 BeagleWebService wsp
= rc
.GetProxy
;
735 Beagle
.Daemon
.HitSnippet
[] hslist
= wsp
.EndGetSnippets(ar
);
738 if (hslist
.Length
> 0)
740 log
.Debug("PrefetchSnippetsResponseHandler: Got {0} snippet responses from {1}", hslist
.Length
, wsp
.Hostname
);
742 foreach (Beagle
.Daemon
.HitSnippet hs
in hslist
) {
749 snippet
= hs
.snippet
;
751 catch (Exception ex2
)
753 log
.Warn ("Exception in WebBackEnd: PrefetchSnippetsResponseHandler(), while getting snippet from {1}\n Reason: {2} ", wsp
.Hostname
+ ":" + wsp
.Port
, ex2
.Message
);
757 if ((hitId
== 0) || (snippet
.StartsWith(WebServiceBackEnd
.InvalidHitSnippetError
)))
760 for (i
= 0; i
< nwHits
.Count
; i
++)
761 if (((NetworkHit
)nwHits
[i
]).Id
== hitId
) {
763 ((NetworkHit
)nwHits
[i
]).snippet
= snippet
;
764 //log.Debug("\nPrefetchSnippetsResponseHandler: URI" + j++ + "=" + ((NetworkHit)nwHits[i]).Uri.ToString() + "\n Snippet=" + snippet);
768 if (i
< nwHits
.Count
)
773 catch (Exception ex
) {
774 log
.Error ("Exception in WebBackEnd: PrefetchSnippetsResponseHandler() - {0} - for {1} ", ex
.Message
, wsp
.Hostname
+ ":" + wsp
.Port
);
777 if (nwHits
.Count
> 0) {
778 //Possible Error in getting snippets for these hitIds
779 log
.Warn("WebBackEnd/PrefetchSnippetsResponseHandler(): Didn't get Snippets for some network Hits");
781 foreach (NetworkHit nh
in nwHits
)
785 //Signal change in TileHitCollection due to addition of snippets:
786 rc
.GetHitCollection
.ClearSources(null);
789 private class ReqContext2
{
791 BT
.TileHitCollection _thc
;
792 BeagleWebService _wsp
;
795 public ReqContext2(BeagleWebService wsp
, ArrayList nwHits
, BT
.TileHitCollection thc
)
799 this._nwHits
= nwHits
;
802 public BT
.TileHitCollection GetHitCollection
{
806 public BeagleWebService GetProxy
{
810 public ArrayList GetNwHits
{
811 get { return _nwHits; }
816 private string GetSearchToken(NetworkHit nh
)
821 string netUri
= nh
.Uri
.ToString();
823 //netbeagle://164.99.153.134:8888/searchToken?http:///....
824 string[] f1
, f2
= netUri
.Split('?');
826 f1
= f2
[0].Split ('/');
828 return (f1
[f1
.Length
- 1]);
833 private string GetHostnamePort(NetworkHit nh
)
838 string netUri
= nh
.Uri
.ToString();
840 //netbeagle://164.99.153.134:8888/searchToken?http:///....
841 string[] f1
, f2
= netUri
.Split('?');
843 f1
= f2
[0].Split ('/');
850 //////////////////////////////////////////////////////////////////////////
852 string static_stylesheet
= "<style type=\"text/css\" media=\"screen\"> body, html { background: white; margin: 0; padding: 0; font-family: Sans, Segoe, Trebuchet MS, Lucida, Sans-Serif; text-align: left; line-height: 1.5em; } a, a:visited { text-decoration: none; color: #2b5a8a; } a:hover { text-decoration: underline; } img { border: 0px; } table { width: 100%; border-collapse: collapse; font-size: 10px; } tr { border-bottom: 1px dotted #999999; } tr:hover { background: #f5f5f5; } tr:hover .icon { background-color: #ddddd0; } td { padding: 6px; } td.icon { background-color: #eeeee0; min-height: 80px; width: 1%; min-width: 80px; text-align: center; vertical-align: top; padding: 12px; } .icon img { max-width: 60px; padding: 4px; } .icon img[src$='.jpg'], img[src$='.jpeg'], img[src*='.thumbnails'] {// max-width: 48px; border: 1px dotted #bbb; // padding: 4px; background: #f9f9f9; } td.content { padding-left: 12px; vertical-align: top; } #hilight { background-color: #ffee66; color: #000000; padding-left: 2px; padding-right: 2px; margin-left: -2px; margin-right: -2px; } .name {font-size: 1.3em; font-weight: bold; color: black; } .date { font-size: 1em; color: black; margin-bottom: 0.6em; margin-top: 0.2em; margin-left: 16px; } .snippet {font-size: 1em; color: gray; margin-left: 16px; } .url {font-size: 1em; color: #008200; margin-left: 16px; } ul {margin-left: 16px; padding: 0px; clear: both; } .actions { font-size: 1em; } .actions li { float: left; display: block; vertical-align: middle; padding: 0; padding-left: 20px; padding-right: 12px; background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/navigation/stock_right.png) no-repeat; min-height: 16px; -moz-opacity: 0.5; } tr:hover .actions li { -moz-opacity: 1.0; } #phone { background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/generic/stock_landline-phone.png) no-repeat; } #email { background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/net/stock_mail.png) no-repeat; } #email-forward { background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/net/stock_mail-forward.png) no-repeat; } #email-reply { background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/net/stock_mail-reply.png) no-repeat; } #message { background: url(file:///opt/gnome/share/icons/hicolor/16x16/apps/im-yahoo.png) no-repeat; } #reveal { background: url(file:///opt/gnome/share/icons/hicolor/16x16/stock/io/stock_open.png) no-repeat; } td.footer { text-align: right; border-bottom: solid 1px white; } </style>";