Guest User

TeamSpeak3_Viewer_Html (Patch)

a guest
Jun 27th, 2011
133
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 19.34 KB | None | 0 0
  1. <?php
  2.  
  3. /**
  4.  * @file
  5.  * TeamSpeak 3 PHP Framework
  6.  *
  7.  * $Id: Text.php 6/4/2011 3:01:20 scp@orilla $
  8.  *
  9.  * This program is free software: you can redistribute it and/or modify
  10.  * it under the terms of the GNU General Public License as published by
  11.  * the Free Software Foundation, either version 3 of the License, or
  12.  * (at your option) any later version.
  13.  *
  14.  * This program is distributed in the hope that it will be useful,
  15.  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16.  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17.  * GNU General Public License for more details.
  18.  *
  19.  * You should have received a copy of the GNU General Public License
  20.  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21.  *
  22.  * @package   TeamSpeak3
  23.  * @version   1.1.6-beta
  24.  * @author    Sven 'ScP' Paulsen
  25.  * @copyright Copyright (c) 2010 by Planet TeamSpeak. All rights reserved.
  26.  */
  27.  
  28. /**
  29.  * @class TeamSpeak3_Viewer_Html
  30.  * @brief Renders nodes used in HTML-based TeamSpeak 3 viewers.
  31.  */
  32. class TeamSpeak3_Viewer_Html implements TeamSpeak3_Viewer_Interface
  33. {
  34.   /**
  35.    * A pre-defined pattern used to display a node in a TeamSpeak 3 viewer.
  36.    *
  37.    * @var string
  38.    */
  39.   protected $pattern = "<table id='%0' class='%1'><tr class='%2'><td class='%3'>%4</td><td class='%5' title='%6'>%7 %8</td><td class='%9'>%10%11</td></tr></table>\n";
  40.  
  41.   /**
  42.    * The TeamSpeak3_Node_Abstract object which is currently processed.
  43.    *
  44.    * @var TeamSpeak3_Node_Abstract
  45.    */
  46.   protected $currObj = null;
  47.  
  48.   /**
  49.    * An array filled with siblingsfor the  TeamSpeak3_Node_Abstract object which is currently
  50.    * processed.
  51.    *
  52.    * @var array
  53.    */
  54.   protected $currSib = null;
  55.  
  56.   /**
  57.    * An internal counter indicating the number of fetched TeamSpeak3_Node_Abstract objects.
  58.    *
  59.    * @var integer
  60.    */
  61.   protected $currNum = 0;
  62.  
  63.   /**
  64.    * The relative URI path where the images used by the viewer can be found.
  65.    *
  66.    * @var string
  67.    */
  68.   protected $iconpath = null;
  69.  
  70.   /**
  71.    * The relative URI path where the country flag icons used by the viewer can be found.
  72.    *
  73.    * @var string
  74.    */
  75.   protected $flagpath = null;
  76.  
  77.   /**
  78.    * The relative path of the file transter client script on the server.
  79.    *
  80.    * @var string
  81.    */
  82.   protected $ftclient = null;
  83.  
  84.   /**
  85.    * Stores an array of local icon IDs.
  86.    *
  87.    * @var array
  88.    */
  89.   protected $cachedIcons = array(100, 200, 300, 400, 500, 600);
  90.  
  91.   /**
  92.    * Stores an array of remote icon IDs.
  93.    *
  94.    * @var array
  95.    */
  96.   protected $remoteIcons = array();
  97.  
  98.   /**
  99.    * The TeamSpeak3_Viewer_Html constructor.
  100.    *
  101.    * @param  string $iconpath
  102.    * @param  string $flagpath
  103.    * @param  string $ftclient
  104.    * @param  string $pattern
  105.    * @return void
  106.    */
  107.   public function __construct($iconpath = "images/viewer/", $flagpath = null, $ftclient = null, $pattern = null)
  108.   {
  109.     $this->iconpath = $iconpath;
  110.     $this->flagpath = $flagpath;
  111.     $this->ftclient = $ftclient;
  112.  
  113.     if($pattern)
  114.     {
  115.       $this->pattern = $pattern;
  116.     }
  117.   }
  118.  
  119.   /**
  120.    * Returns the code needed to display a node in a TeamSpeak 3 viewer.
  121.    *
  122.    * @param  TeamSpeak3_Node_Abstract $node
  123.    * @param  array $siblings
  124.    * @return string
  125.    */
  126.   public function fetchObject(TeamSpeak3_Node_Abstract $node, array $siblings = array())
  127.   {
  128.     $this->currObj = $node;
  129.     $this->currSib = $siblings;
  130.  
  131.     $args = array(
  132.       $this->getContainerIdent(),
  133.       $this->getContainerClass(),
  134.       $this->getRowClass(),
  135.       $this->getPrefixClass(),
  136.       $this->getPrefix(),
  137.       $this->getCorpusClass(),
  138.       $this->getCorpusTitle(),
  139.       $this->getCorpusIcon(),
  140.       $this->getCorpusName(),
  141.       $this->getSuffixClass(),
  142.       $this->getSuffixIcon(),
  143.       $this->getSuffixFlag(),
  144.     );
  145.  
  146.     return TeamSpeak3_Helper_String::factory($this->pattern)->arg($args);
  147.   }
  148.  
  149.   /**
  150.    * Returns a unique identifier for the current node which can be used as a HTML id
  151.    * property.
  152.    *
  153.    * @return string
  154.    */
  155.   protected function getContainerIdent()
  156.   {
  157.     return $this->currObj->getUniqueId();
  158.   }
  159.  
  160.   /**
  161.    * Returns a dynamic string for the current container element which can be used as
  162.    * a HTML class property.
  163.    *
  164.    * @return string
  165.    */
  166.   protected function getContainerClass()
  167.   {
  168.     return "ts3_viewer " . $this->currObj->getClass(null);
  169.   }
  170.  
  171.   /**
  172.    * Returns a dynamic string for the current row element which can be used as a HTML
  173.    * class property.
  174.    *
  175.    * @return string
  176.    */
  177.   protected function getRowClass()
  178.   {
  179.     return ++$this->currNum%2 ? "row1" : "row2";
  180.   }
  181.  
  182.   /**
  183.    * Returns a string for the current prefix element which can be used as a HTML class
  184.    * property.
  185.    *
  186.    * @return string
  187.    */
  188.   protected function getPrefixClass()
  189.   {
  190.     return "prefix " . $this->currObj->getClass(null);
  191.   }
  192.  
  193.   /**
  194.    * Returns the HTML img tags to display the prefix of the current node.
  195.    *
  196.    * @return string
  197.    */
  198.   protected function getPrefix()
  199.   {
  200.     $prefix = "";
  201.  
  202.     if(count($this->currSib))
  203.     {
  204.       $last = array_pop($this->currSib);
  205.  
  206.       foreach($this->currSib as $sibling)
  207.       {
  208.         $prefix .=  ($sibling) ? $this->getImage("tree_line.gif") : $this->getImage("tree_blank.png");
  209.       }
  210.  
  211.       $prefix .= ($last) ? $this->getImage("tree_end.gif") : $this->getImage("tree_mid.gif");
  212.     }
  213.  
  214.     return $prefix;
  215.   }
  216.  
  217.   /**
  218.    * Returns a string for the current corpus element which can be used as a HTML class
  219.    * property. If the current node is a channel spacer the class string will contain
  220.    * additional class names to allow further customization of the content via CSS.
  221.    *
  222.    * @return string
  223.    */
  224.   protected function getCorpusClass()
  225.   {
  226.     $extras = "";
  227.  
  228.     if($this->currObj instanceof TeamSpeak3_Node_Channel && $this->currObj->isSpacer())
  229.     {
  230.       switch($this->currObj->spacerGetType())
  231.       {
  232.         case (string) TeamSpeak3::SPACER_SOLIDLINE:
  233.           $extras .= " solidline";
  234.           break;
  235.  
  236.         case (string) TeamSpeak3::SPACER_DASHLINE:
  237.           $extras .= " dashline";
  238.           break;
  239.  
  240.         case (string) TeamSpeak3::SPACER_DASHDOTLINE:
  241.           $extras .= " dashdotline";
  242.           break;
  243.  
  244.         case (string) TeamSpeak3::SPACER_DASHDOTDOTLINE:
  245.           $extras .= " dashdotdotline";
  246.           break;
  247.  
  248.         case (string) TeamSpeak3::SPACER_DOTLINE:
  249.           $extras .= " dotline";
  250.           break;
  251.       }
  252.  
  253.       switch($this->currObj->spacerGetAlign())
  254.       {
  255.         case TeamSpeak3::SPACER_ALIGN_CENTER:
  256.           $extras .= " center";
  257.           break;
  258.  
  259.         case TeamSpeak3::SPACER_ALIGN_RIGHT:
  260.           $extras .= " right";
  261.           break;
  262.  
  263.         case TeamSpeak3::SPACER_ALIGN_LEFT:
  264.           $extras .= " left";
  265.           break;
  266.       }
  267.     }
  268.  
  269.     return "corpus " . $this->currObj->getClass(null) . $extras;
  270.   }
  271.  
  272.   /**
  273.    * Returns the HTML img tags which can be used to display the various icons for a
  274.    * TeamSpeak_Node_Abstract object.
  275.    *
  276.    * @return string
  277.    */
  278.   protected function getCorpusTitle()
  279.   {
  280.     if($this->currObj instanceof TeamSpeak3_Node_Server)
  281.     {
  282.       return "ID: " . $this->currObj->getId() . " | Clients: " . $this->currObj->clientCount() . "/" . $this->currObj["virtualserver_maxclients"] . " | Uptime: " . TeamSpeak3_Helper_Convert::seconds($this->currObj["virtualserver_uptime"]);
  283.     }
  284.     elseif($this->currObj instanceof TeamSpeak3_Node_Channel && !$this->currObj->isSpacer())
  285.     {
  286.       return "ID: " . $this->currObj->getId() . " | Codec: " . TeamSpeak3_Helper_Convert::codec($this->currObj["channel_codec"]) . " | Quality: " . $this->currObj["channel_codec_quality"];
  287.     }
  288.     elseif($this->currObj instanceof TeamSpeak3_Node_Client)
  289.     {
  290.       return "ID: " . $this->currObj->getId() . " | Version: " . $this->currObj["client_version"] . " | Platform: " . $this->currObj["client_platform"];
  291.     }
  292.     elseif($this->currObj instanceof TeamSpeak3_Node_Servergroup || $this->currObj instanceof TeamSpeak3_Node_Channelgroup)
  293.     {
  294.       return "ID: " . $this->currObj->getId() . " | Type: " . TeamSpeak3_Helper_Convert::groupType($this->currObj["type"]) . " (" . ($this->currObj["savedb"] ? "Permanent" : "Temporary") . ")";
  295.     }
  296.   }
  297.  
  298.   /**
  299.    * Returns a HTML img tag which can be used to display the status icon for a
  300.    * TeamSpeak_Node_Abstract object.
  301.    *
  302.    * @return string
  303.    */
  304.   protected function getCorpusIcon()
  305.   {
  306.     if($this->currObj instanceof TeamSpeak3_Node_Channel && $this->currObj->isSpacer()) return;
  307.  
  308.     return $this->getImage($this->currObj->getIcon() . ".png");
  309.   }
  310.  
  311.   /**
  312.    * Returns a string for the current corpus element which contains the display name
  313.    * for the current TeamSpeak_Node_Abstract object.
  314.    *
  315.    * @return string
  316.    */
  317.   protected function getCorpusName()
  318.   {
  319.     if($this->currObj instanceof TeamSpeak3_Node_Channel && $this->currObj->isSpacer())
  320.     {
  321.       if($this->currObj->spacerGetType() != TeamSpeak3::SPACER_CUSTOM) return;
  322.  
  323.       $string = $this->currObj["channel_name"]->section("]", 1);
  324.  
  325.       if($this->currObj->spacerGetAlign() == TeamSpeak3::SPACER_ALIGN_REPEAT)
  326.       {
  327.         $string->resize(30, $string);
  328.       }
  329.  
  330.       return htmlspecialchars($string);
  331.     }
  332.  
  333.     if($this->currObj instanceof TeamSpeak3_Node_Client)
  334.     {
  335.       $before = array();
  336.       $behind = array();
  337.  
  338.       foreach($this->currObj->memberOf() as $group)
  339.       {
  340.         if($group->getProperty("namemode") == TeamSpeak3::GROUP_NAMEMODE_BEFORE)
  341.         {
  342.           $before[] = "[" . htmlspecialchars($group["name"]) . "]";
  343.         }
  344.         elseif($group->getProperty("namemode") == TeamSpeak3::GROUP_NAMEMODE_BEHIND)
  345.         {
  346.           $behind[] = "[" . htmlspecialchars($group["name"]) . "]";
  347.         }
  348.       }
  349.  
  350.       return implode("", $before) . " " . htmlspecialchars($this->currObj) . " " . implode("", $behind);
  351.     }
  352.  
  353.     return htmlspecialchars($this->currObj);
  354.   }
  355.  
  356.   /**
  357.    * Returns a string for the current suffix element which can be used as a HTML
  358.    * class property.
  359.    *
  360.    * @return string
  361.    */
  362.   protected function getSuffixClass()
  363.   {
  364.     return "suffix " . $this->currObj->getClass(null);
  365.   }
  366.  
  367.   /**
  368.    * Returns the HTML img tags which can be used to display the various icons for a
  369.    * TeamSpeak_Node_Abstract object.
  370.    *
  371.    * @return string
  372.    */
  373.   protected function getSuffixIcon()
  374.   {
  375.     if($this->currObj instanceof TeamSpeak3_Node_Server)
  376.     {
  377.       return $this->getSuffixIconServer();
  378.     }
  379.     elseif($this->currObj instanceof TeamSpeak3_Node_Channel)
  380.     {
  381.       return $this->getSuffixIconChannel();
  382.     }
  383.     elseif($this->currObj instanceof TeamSpeak3_Node_Client)
  384.     {
  385.       return $this->getSuffixIconClient();
  386.     }
  387.   }
  388.  
  389.   /**
  390.    * Returns the HTML img tags which can be used to display the various icons for a
  391.    * TeamSpeak_Node_Server object.
  392.    *
  393.    * @return string
  394.    */
  395.   protected function getSuffixIconServer()
  396.   {
  397.     $html = "";
  398.  
  399.     if($this->currObj["virtualserver_icon_id"])
  400.     {
  401.       if(!$this->currObj->iconIsLocal("virtualserver_icon_id") && $this->ftclient)
  402.       {
  403.         if(!isset($this->cacheIcon[$this->currObj["virtualserver_icon_id"]]))
  404.         {
  405.           $download = $this->currObj->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName("virtualserver_icon_id"));
  406.  
  407.           if($this->ftclient == "data:image")
  408.           {
  409.             $download = TeamSpeak3::factory("filetransfer://" . $download["host"] . ":" . $download["port"])->download($download["ftkey"], $download["size"]);
  410.           }
  411.  
  412.           $this->cacheIcon[$this->currObj["virtualserver_icon_id"]] = $download;
  413.         }
  414.         else
  415.         {
  416.           $download = $this->cacheIcon[$this->currObj["virtualserver_icon_id"]];
  417.         }
  418.  
  419.         if($this->ftclient == "data:image")
  420.         {
  421.           $html .= $this->getImage("data:" . TeamSpeak3_Helper_Convert::imageMimeType($download) . ";base64," . base64_encode($download), "Server Icon", null, FALSE);
  422.         }
  423.         else
  424.         {
  425.           $html .= $this->getImage($this->ftclient . "?ftdata=" . base64_encode(serialize($download)), "Server Icon", null, FALSE);
  426.         }
  427.       }
  428.       elseif(in_array($this->currObj["virtualserver_icon_id"], $this->cachedIcons))
  429.       {
  430.         $html .= $this->getImage("group_icon_" . $this->currObj["virtualserver_icon_id"] . ".png", "Server Icon");
  431.       }
  432.     }
  433.  
  434.     return $html;
  435.   }
  436.  
  437.   /**
  438.    * Returns the HTML img tags which can be used to display the various icons for a
  439.    * TeamSpeak_Node_Channel object.
  440.    *
  441.    * @return string
  442.    */
  443.   protected function getSuffixIconChannel()
  444.   {
  445.     if($this->currObj instanceof TeamSpeak3_Node_Channel && $this->currObj->isSpacer()) return;
  446.  
  447.     $html = "";
  448.  
  449.     if($this->currObj["channel_flag_default"])
  450.     {
  451.       $html .= $this->getImage("channel_flag_default.png", "Default Channel");
  452.     }
  453.  
  454.     if($this->currObj["channel_flag_password"])
  455.     {
  456.       $html .= $this->getImage("channel_flag_password.png", "Password-protected");
  457.     }
  458.  
  459.     if($this->currObj["channel_codec"] == 3)
  460.     {
  461.       $html .= $this->getImage("channel_flag_music.png", "Music Codec");
  462.     }
  463.  
  464.     if($this->currObj["channel_needed_talk_power"])
  465.     {
  466.       $html .= $this->getImage("channel_flag_moderated.png", "Moderated");
  467.     }
  468.  
  469.     if($this->currObj["channel_icon_id"])
  470.     {
  471.       if(!$this->currObj->iconIsLocal("channel_icon_id") && $this->ftclient)
  472.       {
  473.         if(!isset($this->cacheIcon[$this->currObj["channel_icon_id"]]))
  474.         {
  475.           $download = $this->currObj->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName("channel_icon_id"));
  476.  
  477.           if($this->ftclient == "data:image")
  478.           {
  479.             $download = TeamSpeak3::factory("filetransfer://" . $download["host"] . ":" . $download["port"])->download($download["ftkey"], $download["size"]);
  480.           }
  481.  
  482.           $this->cacheIcon[$this->currObj["channel_icon_id"]] = $download;
  483.         }
  484.         else
  485.         {
  486.           $download = $this->cacheIcon[$this->currObj["channel_icon_id"]];
  487.         }
  488.  
  489.         if($this->ftclient == "data:image")
  490.         {
  491.           $html .= $this->getImage("data:" . TeamSpeak3_Helper_Convert::imageMimeType($download) . ";base64," . base64_encode($download), "Channel Icon", null, FALSE);
  492.         }
  493.         else
  494.         {
  495.           $html .= $this->getImage($this->ftclient . "?ftdata=" . base64_encode(serialize($download)), "Channel Icon", null, FALSE);
  496.         }
  497.       }
  498.       elseif(in_array($this->currObj["channel_icon_id"], $this->cachedIcons))
  499.       {
  500.         $html .= $this->getImage("group_icon_" . $this->currObj["channel_icon_id"] . ".png", "Channel Icon");
  501.       }
  502.     }
  503.  
  504.     return $html;
  505.   }
  506.  
  507.   /**
  508.    * Returns the HTML img tags which can be used to display the various icons for a
  509.    * TeamSpeak_Node_Client object.
  510.    *
  511.    * @return string
  512.    */
  513.   protected function getSuffixIconClient()
  514.   {
  515.     $html = "";
  516.  
  517.     if($this->currObj["client_is_priority_speaker"])
  518.     {
  519.       $html .= $this->getImage("client_priority.png", "Priority Speaker");
  520.     }
  521.  
  522.     if($this->currObj["client_is_channel_commander"])
  523.     {
  524.       $html .= $this->getImage("client_cc.png", "Channel Commander");
  525.     }
  526.  
  527.     if($this->currObj["client_is_talker"])
  528.     {
  529.       $html .= $this->getImage("client_talker.png", "Talk Power granted");
  530.     }
  531.  
  532.     foreach($this->currObj->memberOf() as $group)
  533.     {
  534.       if(!$group["iconid"]) continue;
  535.  
  536.       $type = ($group instanceof TeamSpeak3_Node_Servergroup) ? "Server Group" : "Channel Group";
  537.  
  538.       if(!$group->iconIsLocal("iconid") && $this->ftclient)
  539.       {
  540.         if(!isset($this->cacheIcon[$group["iconid"]]))
  541.         {
  542.           $download = $group->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $group->iconGetName("iconid"));
  543.  
  544.           if($this->ftclient == "data:image")
  545.           {
  546.             $download = TeamSpeak3::factory("filetransfer://" . $download["host"] . ":" . $download["port"])->download($download["ftkey"], $download["size"]);
  547.           }
  548.  
  549.           $this->cacheIcon[$group["iconid"]] = $download;
  550.         }
  551.         else
  552.         {
  553.           $download = $this->cacheIcon[$group["iconid"]];
  554.         }
  555.  
  556.         if($this->ftclient == "data:image")
  557.         {
  558.           $html .= $this->getImage("data:" . TeamSpeak3_Helper_Convert::imageMimeType($download) . ";base64," . base64_encode($download), $group . " [" . $type . "]", null, FALSE);
  559.         }
  560.         else
  561.         {
  562.           $html .= $this->getImage($this->ftclient . "?ftdata=" . base64_encode(serialize($download)), $group . " [" . $type . "]", null, FALSE);
  563.         }
  564.       }
  565.       elseif(in_array($group["iconid"], $this->cachedIcons))
  566.       {
  567.         $html .= $this->getImage("group_icon_" . $group["iconid"] . ".png", $group . " [" . $type . "]");
  568.       }
  569.     }
  570.  
  571.     if($this->currObj["client_icon_id"])
  572.     {
  573.       if(!$this->currObj->iconIsLocal("client_icon_id") && $this->ftclient)
  574.       {
  575.         if(!isset($this->cacheIcon[$this->currObj["client_icon_id"]]))
  576.         {
  577.           $download = $this->currObj->getParent()->transferInitDownload(rand(0x0000, 0xFFFF), 0, $this->currObj->iconGetName("client_icon_id"));
  578.  
  579.           if($this->ftclient == "data:image")
  580.           {
  581.             $download = TeamSpeak3::factory("filetransfer://" . $download["host"] . ":" . $download["port"])->download($download["ftkey"], $download["size"]);
  582.           }
  583.  
  584.           $this->cacheIcon[$this->currObj["client_icon_id"]] = $download;
  585.         }
  586.         else
  587.         {
  588.           $download = $this->cacheIcon[$this->currObj["client_icon_id"]];
  589.         }
  590.  
  591.         if($this->ftclient == "data:image")
  592.         {
  593.           $html .= $this->getImage("data:" . TeamSpeak3_Helper_Convert::imageMimeType($download) . ";base64," . base64_encode($download), "Client Icon", null, FALSE);
  594.         }
  595.         else
  596.         {
  597.           $html .= $this->getImage($this->ftclient . "?ftdata=" . base64_encode(serialize($download)), "Client Icon", null, FALSE);
  598.         }
  599.       }
  600.       elseif(in_array($this->currObj["client_icon_id"], $this->cachedIcons))
  601.       {
  602.         $html .= $this->getImage("group_icon_" . $this->currObj["client_icon_id"] . ".png", "Client Icon");
  603.       }
  604.     }
  605.  
  606.     return $html;
  607.   }
  608.  
  609.   /**
  610.    * Returns a HTML img tag which can be used to display the country flag for a
  611.    * TeamSpeak_Node_Client object.
  612.    *
  613.    * @return string
  614.    */
  615.   protected function getSuffixFlag()
  616.   {
  617.     if(!$this->currObj instanceof TeamSpeak3_Node_Client) return;
  618.  
  619.     if($this->flagpath && $this->currObj["client_country"])
  620.     {
  621.       return $this->getImage($this->currObj["client_country"]->toLower() . ".png", $this->currObj["client_country"], null, FALSE, TRUE);
  622.     }
  623.   }
  624.  
  625.   /**
  626.    * Returns the code to display a custom HTML img tag.
  627.    *
  628.    * @param  string  $name
  629.    * @param  string  $text
  630.    * @param  string  $class
  631.    * @param  boolean $iconpath
  632.    * @param  boolean $flagpath
  633.    * @return string
  634.    */
  635.   protected function getImage($name, $text = "", $class = null, $iconpath = TRUE, $flagpath = FALSE)
  636.   {
  637.     $src = "";
  638.  
  639.     if($iconpath)
  640.     {
  641.       $src = $this->iconpath;
  642.     }
  643.  
  644.     if($flagpath)
  645.     {
  646.       $src = $this->flagpath;
  647.     }
  648.  
  649.     return "<img src='" . $src . $name . "' title='" . $text . "' alt='' align='top' />";
  650.   }
  651. }
Add Comment
Please, Sign In to add comment