Skip to content

Instantly share code, notes, and snippets.

@jaseclamp
Created April 8, 2014 02:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaseclamp/10083937 to your computer and use it in GitHub Desktop.
Save jaseclamp/10083937 to your computer and use it in GitHub Desktop.
/app/code/local/Mage/CatalogSearch/Model/Resource/Fulltext.php
<?php
/**
* Magento
*
* NOTICE OF LICENSE
*
* This source file is subject to the Open Software License (OSL 3.0)
* that is bundled with this package in the file LICENSE.txt.
* It is also available through the world-wide-web at this URL:
* http://opensource.org/licenses/osl-3.0.php
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@magentocommerce.com so we can send you a copy immediately.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade Magento to newer
* versions in the future. If you wish to customize Magento for your
* needs please refer to http://www.magentocommerce.com for more information.
*
* @category Mage
* @package Mage_CatalogSearch
* @copyright Copyright (c) 2012 Magento Inc. (http://www.magentocommerce.com)
* @license http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
*/
/**
* CatalogSearch Fulltext Index resource model
*
* @category Mage
* @package Mage_CatalogSearch
* @author Magento Core Team <core@magentocommerce.com>
*/
class Mage_CatalogSearch_Model_Resource_Fulltext extends Mage_Core_Model_Resource_Db_Abstract
{
/**
* Searchable attributes cache
*
* @var array
*/
protected $_searchableAttributes = null;
/**
* Index values separator
*
* @var string
*/
protected $_separator = '|';
/**
* Array of Zend_Date objects per store
*
* @var array
*/
protected $_dates = array();
/**
* Product Type Instances cache
*
* @var array
*/
protected $_productTypes = array();
/**
* Store search engine instance
*
* @var object
*/
protected $_engine = null;
/**
* Whether table changes are allowed
*
* @deprecated after 1.6.1.0
* @var bool
*/
protected $_allowTableChanges = true;
/**
* Init resource model
*
*/
protected function _construct()
{
$this->_init('catalogsearch/fulltext', 'product_id');
$this->_engine = Mage::helper('catalogsearch')->getEngine();
}
/**
* Return options separator
*
* @return string
*/
public function getSeparator()
{
return $this->_separator;
}
/**
* Regenerate search index for store(s)
*
* @param int|null $storeId
* @param int|array|null $productIds
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
public function rebuildIndex($storeId = null, $productIds = null)
{
if (is_null($storeId)) {
$storeIds = array_keys(Mage::app()->getStores());
foreach ($storeIds as $storeId) {
$this->_rebuildStoreIndex($storeId, $productIds);
}
} else {
$this->_rebuildStoreIndex($storeId, $productIds);
}
return $this;
}
/**
* Regenerate search index for specific store
*
* @param int $storeId Store View Id
* @param int|array $productIds Product Entity Id
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
protected function _rebuildStoreIndex($storeId, $productIds = null)
{
$this->cleanIndex($storeId, $productIds);
// prepare searchable attributes
$staticFields = array();
foreach ($this->_getSearchableAttributes('static') as $attribute) {
$staticFields[] = $attribute->getAttributeCode();
}
$dynamicFields = array(
'int' => array_keys($this->_getSearchableAttributes('int')),
'varchar' => array_keys($this->_getSearchableAttributes('varchar')),
'text' => array_keys($this->_getSearchableAttributes('text')),
'decimal' => array_keys($this->_getSearchableAttributes('decimal')),
'datetime' => array_keys($this->_getSearchableAttributes('datetime')),
);
// status and visibility filter
$visibility = $this->_getSearchableAttribute('visibility');
$status = $this->_getSearchableAttribute('status');
$statusVals = Mage::getSingleton('catalog/product_status')->getVisibleStatusIds();
$allowedVisibilityValues = $this->_engine->getAllowedVisibility();
$lastProductId = 0;
while (true) {
$products = $this->_getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId);
if (!$products) {
break;
}
$productAttributes = array();
$productRelations = array();
foreach ($products as $productData) {
$lastProductId = $productData['entity_id'];
$productAttributes[$productData['entity_id']] = $productData['entity_id'];
$productChildren = $this->_getProductChildIds($productData['entity_id'], $productData['type_id']);
$productRelations[$productData['entity_id']] = $productChildren;
if ($productChildren) {
foreach ($productChildren as $productChildId) {
$productAttributes[$productChildId] = $productChildId;
}
}
}
$productIndexes = array();
$productAttributes = $this->_getProductAttributes($storeId, $productAttributes, $dynamicFields);
foreach ($products as $productData) {
if (!isset($productAttributes[$productData['entity_id']])) {
continue;
}
$productAttr = $productAttributes[$productData['entity_id']];
if (!isset($productAttr[$visibility->getId()])
|| !in_array($productAttr[$visibility->getId()], $allowedVisibilityValues)
) {
continue;
}
if (!isset($productAttr[$status->getId()]) || !in_array($productAttr[$status->getId()], $statusVals)) {
continue;
}
$productIndex = array(
$productData['entity_id'] => $productAttr
);
if ($productChildren = $productRelations[$productData['entity_id']]) {
foreach ($productChildren as $productChildId) {
if (isset($productAttributes[$productChildId])) {
$productIndex[$productChildId] = $productAttributes[$productChildId];
}
}
}
$index = $this->_prepareProductIndex($productIndex, $productData, $storeId);
$productIndexes[$productData['entity_id']] = $index;
}
$this->_saveProductIndexes($storeId, $productIndexes);
}
$this->resetSearchResults();
return $this;
}
/**
* Retrieve searchable products per store
*
* @param int $storeId
* @param array $staticFields
* @param array|int $productIds
* @param int $lastProductId
* @param int $limit
* @return array
*/
protected function _getSearchableProducts($storeId, array $staticFields, $productIds = null, $lastProductId = 0,
$limit = 100)
{
$websiteId = Mage::app()->getStore($storeId)->getWebsiteId();
$writeAdapter = $this->_getWriteAdapter();
$select = $writeAdapter->select()
->useStraightJoin(true)
->from(
array('e' => $this->getTable('catalog/product')),
array_merge(array('entity_id', 'type_id'), $staticFields)
)
->join(
array('website' => $this->getTable('catalog/product_website')),
$writeAdapter->quoteInto(
'website.product_id=e.entity_id AND website.website_id=?',
$websiteId
),
array()
)
->join(
array('stock_status' => $this->getTable('cataloginventory/stock_status')),
$writeAdapter->quoteInto(
'stock_status.product_id=e.entity_id AND stock_status.website_id=?',
$websiteId
),
array('in_stock' => 'stock_status')
);
if (!is_null($productIds)) {
$select->where('e.entity_id IN(?)', $productIds);
}
$select->where('e.entity_id>?', $lastProductId)
->limit($limit)
->order('e.entity_id');
$result = $writeAdapter->fetchAll($select);
return $result;
}
/**
* Reset search results
*
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
public function resetSearchResults()
{
$adapter = $this->_getWriteAdapter();
$adapter->update($this->getTable('catalogsearch/search_query'), array('is_processed' => 0));
$adapter->delete($this->getTable('catalogsearch/result'));
Mage::dispatchEvent('catalogsearch_reset_search_result');
return $this;
}
/**
* Delete search index data for store
*
* @param int $storeId Store View Id
* @param int $productId Product Entity Id
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
public function cleanIndex($storeId = null, $productId = null)
{
if ($this->_engine) {
$this->_engine->cleanIndex($storeId, $productId);
}
return $this;
}
/**
* Prepare results for query
*
* @param Mage_CatalogSearch_Model_Fulltext $object
* @param string $queryText
* @param Mage_CatalogSearch_Model_Query $query
* @return Mage_CatalogSearch_Model_Resource_Fulltext
/home/mad-prod/public_html/catalogue/app/code/core/Mage/CatalogSearch/Model/Resource
*/
public function prepareResult($object, $queryText, $query)
{
$adapter = $this->_getWriteAdapter();
$reader = $this->_getReadAdapter();
if (!$query->getIsProcessed()) {
$searchType = $object->getSearchType($query->getStoreId());
$preparedTerms = Mage::getResourceHelper('catalogsearch')
->prepareTerms($queryText, $query->getMaxQueryWords());
//BEGIN JASE ADDITION
//get every possible sequential phrase from the entire search query
foreach( $s = array_values($preparedTerms[1]) as $key => $word)
{
$r = array();
for ($i = $key; $i < count($s); $i++)
{
$r[] = $s[$i];
if(count($r) > 1) $phrases[] = implode(' ', $r);
}
$phrases[] = $word;
}
//NOTE the attribute id numbers below probably need to be made dynamic...
//now get brands...
$sql = "SELECT o.option_id, v.value FROM `eav_attribute_option` o JOIN eav_attribute_option_value v ON o.option_id = v.option_id WHERE o.`attribute_id` = 81 AND v.store_id = ".$query->getStoreId();
$brands = $reader->fetchAll($sql);
//now get categories...
$sql = "SELECT value, entity_id from catalog_category_entity_varchar where attribute_id = 41";
$categories = $reader->fetchAll($sql);
//go through each phrase
foreach($phrases as $phrase)
{
foreach($brands as $brand)
if( strpos( strtolower($phrase) , strtolower($brand['value']) ) !== false ) //is brand found in phrase?
{
$brandFound[] = $brand['option_id'];
$remove[] = $phrase;
}
foreach($categories as $category)
if( str_word_count($phrase) >= 2) //maybe lets only check double word phrases...
if( strpos( strtolower($category['value']) , strtolower($phrase) ) !== false ) //is phrase found in category?
{
$categoryFound[] = $category['entity_id'];
$remove[] = $phrase;
}
}
//remove brands and categories from query string
foreach($remove as $rem)
$queryText = str_replace($rem,"",$queryText);
//if there's no actual text left, make the query string match a phrase which is found in all products.
//This can be accomplished by making product status a searchable field so that "Enabled" is always in the fulltext.
preg_match_all("/[a-z0-9]/",$queryText,$matches);
if(count($matches[0])<=3) $queryText = "Enabled";
//END JASE ADDITION
$bind = array();
$like = array();
$likeCond = '';
if ($searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_LIKE
|| $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE
) {
$helper = Mage::getResourceHelper('core');
$words = Mage::helper('core/string')->splitWords($queryText, true, $query->getMaxQueryWords());
foreach ($words as $word) {
$like[] = $helper->getCILike('s.data_index', $word, array('position' => 'any'));
}
if ($like) {
$likeCond = '(' . join(' AND ', $like) . ')'; //this is changed to AND from OR in the core version
}
}
$mainTableAlias = 's';
$fields = array(
'query_id' => new Zend_Db_Expr($query->getId()),
'product_id',
);
$select = $adapter->select()
->from(array($mainTableAlias => $this->getMainTable()), $fields)
->joinInner(array('e' => $this->getTable('catalog/product')),
'e.entity_id = s.product_id',
array());
//BEGIN JASE ADDITION
//if there was a brand found, make that a filter...
if(isset($brandFound)) {
$select->joinRight(array('b' => 'catalog_product_entity_int' ),
'b.entity_id = s.product_id AND b.attribute_id = 81',
array());
$select->where( "b.value IN (".implode(",",$brandFound).")" ) ;
}
//if there was a category found, make that a filter...
if(isset($categoryFound)) {
$select->joinRight(array('c' => 'catalog_category_product' ),
'c.product_id = s.product_id',
array());
$select->where( "c.category_id IN (".implode(",",$categoryFound).")" );
}
//END JASE ADDITION
$select->where($mainTableAlias.'.store_id = ?', (int)$query->getStoreId());
if ($searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_FULLTEXT
|| $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE
) {
$bind[':query'] = implode(' ', $preparedTerms[0]);
$where = Mage::getResourceHelper('catalogsearch')
->chooseFulltext($this->getMainTable(), $mainTableAlias, $select);
}
if ($likeCond != '' && $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_COMBINE) {
$where .= ($where ? ' OR ' : '') . $likeCond;
} elseif ($likeCond != '' && $searchType == Mage_CatalogSearch_Model_Fulltext::SEARCH_TYPE_LIKE) {
$select->columns(array('relevance' => new Zend_Db_Expr(0)));
$where = $likeCond;
}
if ($where != '') {
$select->where($where);
}
$sql = $adapter->insertFromSelect($select,
$this->getTable('catalogsearch/result'),
array(),
Varien_Db_Adapter_Interface::INSERT_ON_DUPLICATE);
//echo "<pre>"; print_r($sql); die;
$adapter->query($sql, $bind);
$query->setIsProcessed(1);
}
return $this;
}
/**
* Retrieve EAV Config Singleton
*
* @return Mage_Eav_Model_Config
*/
public function getEavConfig()
{
return Mage::getSingleton('eav/config');
}
/**
* Retrieve searchable attributes
*
* @param string $backendType
* @return array
*/
protected function _getSearchableAttributes($backendType = null)
{
if (is_null($this->_searchableAttributes)) {
$this->_searchableAttributes = array();
$productAttributeCollection = Mage::getResourceModel('catalog/product_attribute_collection');
if ($this->_engine && $this->_engine->allowAdvancedIndex()) {
$productAttributeCollection->addToIndexFilter(true);
} else {
$productAttributeCollection->addSearchableAttributeFilter();
}
$attributes = $productAttributeCollection->getItems();
Mage::dispatchEvent('catelogsearch_searchable_attributes_load_after', array(
'engine' => $this->_engine,
'attributes' => $attributes
));
$entity = $this->getEavConfig()
->getEntityType(Mage_Catalog_Model_Product::ENTITY)
->getEntity();
foreach ($attributes as $attribute) {
$attribute->setEntity($entity);
}
$this->_searchableAttributes = $attributes;
}
if (!is_null($backendType)) {
$attributes = array();
foreach ($this->_searchableAttributes as $attributeId => $attribute) {
if ($attribute->getBackendType() == $backendType) {
$attributes[$attributeId] = $attribute;
}
}
return $attributes;
}
return $this->_searchableAttributes;
}
/**
* Retrieve searchable attribute by Id or code
*
* @param int|string $attribute
* @return Mage_Eav_Model_Entity_Attribute
*/
protected function _getSearchableAttribute($attribute)
{
$attributes = $this->_getSearchableAttributes();
if (is_numeric($attribute)) {
if (isset($attributes[$attribute])) {
return $attributes[$attribute];
}
} elseif (is_string($attribute)) {
foreach ($attributes as $attributeModel) {
if ($attributeModel->getAttributeCode() == $attribute) {
return $attributeModel;
}
}
}
return $this->getEavConfig()->getAttribute(Mage_Catalog_Model_Product::ENTITY, $attribute);
}
/**
* Returns expresion for field unification
*
* @param string $field
* @param string $backendType
* @return Zend_Db_Expr
*/
protected function _unifyField($field, $backendType = 'varchar')
{
if ($backendType == 'datetime') {
$expr = Mage::getResourceHelper('catalogsearch')->castField(
$this->_getReadAdapter()->getDateFormatSql($field, '%Y-%m-%d %H:%i:%s'));
} else {
$expr = Mage::getResourceHelper('catalogsearch')->castField($field);
}
return $expr;
}
/**
* Load product(s) attributes
*
* @param int $storeId
* @param array $productIds
* @param array $attributeTypes
* @return array
*/
protected function _getProductAttributes($storeId, array $productIds, array $attributeTypes)
{
$result = array();
$selects = array();
$adapter = $this->_getWriteAdapter();
$ifStoreValue = $adapter->getCheckSql('t_store.value_id > 0', 't_store.value', 't_default.value');
foreach ($attributeTypes as $backendType => $attributeIds) {
if ($attributeIds) {
$tableName = $this->getTable(array('catalog/product', $backendType));
$selects[] = $adapter->select()
->from(
array('t_default' => $tableName),
array('entity_id', 'attribute_id'))
->joinLeft(
array('t_store' => $tableName),
$adapter->quoteInto(
't_default.entity_id=t_store.entity_id' .
' AND t_default.attribute_id=t_store.attribute_id' .
' AND t_store.store_id=?',
$storeId),
array('value' => $this->_unifyField($ifStoreValue, $backendType)))
->where('t_default.store_id=?', 0)
->where('t_default.attribute_id IN (?)', $attributeIds)
->where('t_default.entity_id IN (?)', $productIds);
}
}
if ($selects) {
$select = $adapter->select()->union($selects, Zend_Db_Select::SQL_UNION_ALL);
$query = $adapter->query($select);
while ($row = $query->fetch()) {
$result[$row['entity_id']][$row['attribute_id']] = $row['value'];
}
}
return $result;
}
/**
* Retrieve Product Type Instance
*
* @param string $typeId
* @return Mage_Catalog_Model_Product_Type_Abstract
*/
protected function _getProductTypeInstance($typeId)
{
if (!isset($this->_productTypes[$typeId])) {
$productEmulator = $this->_getProductEmulator();
$productEmulator->setTypeId($typeId);
$this->_productTypes[$typeId] = Mage::getSingleton('catalog/product_type')
->factory($productEmulator);
}
return $this->_productTypes[$typeId];
}
/**
* Return all product children ids
*
* @param int $productId Product Entity Id
* @param string $typeId Super Product Link Type
* @return array
*/
protected function _getProductChildIds($productId, $typeId)
{
$typeInstance = $this->_getProductTypeInstance($typeId);
$relation = $typeInstance->isComposite()
? $typeInstance->getRelationInfo()
: false;
if ($relation && $relation->getTable() && $relation->getParentFieldName() && $relation->getChildFieldName()) {
$select = $this->_getReadAdapter()->select()
->from(
array('main' => $this->getTable($relation->getTable())),
array($relation->getChildFieldName()))
->where("{$relation->getParentFieldName()}=?", $productId);
if (!is_null($relation->getWhere())) {
$select->where($relation->getWhere());
}
return $this->_getReadAdapter()->fetchCol($select);
}
return null;
}
/**
* Retrieve Product Emulator (Varien Object)
*
* @return Varien_Object
*/
protected function _getProductEmulator()
{
$productEmulator = new Varien_Object();
$productEmulator->setIdFieldName('entity_id');
return $productEmulator;
}
/**
* Prepare Fulltext index value for product
*
* @param array $indexData
* @param array $productData
* @param int $storeId
* @return string
*/
protected function _prepareProductIndex($indexData, $productData, $storeId)
{
$index = array();
foreach ($this->_getSearchableAttributes('static') as $attribute) {
$attributeCode = $attribute->getAttributeCode();
if (isset($productData[$attributeCode])) {
$value = $this->_getAttributeValue($attribute->getId(), $productData[$attributeCode], $storeId);
if ($value) {
//For grouped products
if (isset($index[$attributeCode])) {
if (!is_array($index[$attributeCode])) {
$index[$attributeCode] = array($index[$attributeCode]);
}
$index[$attributeCode][] = $value;
}
//For other types of products
else {
$index[$attributeCode] = $value;
}
}
}
}
foreach ($indexData as $entityId => $attributeData) {
foreach ($attributeData as $attributeId => $attributeValue) {
$value = $this->_getAttributeValue($attributeId, $attributeValue, $storeId);
if (!is_null($value) && $value !== false) {
$attributeCode = $this->_getSearchableAttribute($attributeId)->getAttributeCode();
if (isset($index[$attributeCode])) {
$index[$attributeCode][$entityId] = $value;
} else {
$index[$attributeCode] = array($entityId => $value);
}
}
}
}
if (!$this->_engine->allowAdvancedIndex()) {
$product = $this->_getProductEmulator()
->setId($productData['entity_id'])
->setTypeId($productData['type_id'])
->setStoreId($storeId);
$typeInstance = $this->_getProductTypeInstance($productData['type_id']);
if ($data = $typeInstance->getSearchableData($product)) {
$index['options'] = $data;
}
}
if (isset($productData['in_stock'])) {
$index['in_stock'] = $productData['in_stock'];
}
if ($this->_engine) {
return $this->_engine->prepareEntityIndex($index, $this->_separator);
}
return Mage::helper('catalogsearch')->prepareIndexdata($index, $this->_separator);
}
/**
* Retrieve attribute source value for search
*
* @param int $attributeId
* @param mixed $value
* @param int $storeId
* @return mixed
*/
protected function _getAttributeValue($attributeId, $value, $storeId)
{
$attribute = $this->_getSearchableAttribute($attributeId);
if (!$attribute->getIsSearchable()) {
if ($this->_engine->allowAdvancedIndex()) {
if ($attribute->getAttributeCode() == 'visibility') {
return $value;
} elseif (!($attribute->getIsVisibleInAdvancedSearch()
|| $attribute->getIsFilterable()
|| $attribute->getIsFilterableInSearch()
|| $attribute->getUsedForSortBy())
) {
return null;
}
} else {
return null;
}
}
if ($attribute->usesSource()) {
if ($this->_engine->allowAdvancedIndex()) {
return $value;
}
$attribute->setStoreId($storeId);
$value = $attribute->getSource()->getOptionText($value);
if (is_array($value)) {
$value = implode($this->_separator, $value);
} elseif (empty($value)) {
$inputType = $attribute->getFrontend()->getInputType();
if ($inputType == 'select' || $inputType == 'multiselect') {
return null;
}
}
} elseif ($attribute->getBackendType() == 'datetime') {
$value = $this->_getStoreDate($storeId, $value);
} else {
$inputType = $attribute->getFrontend()->getInputType();
if ($inputType == 'price') {
$value = Mage::app()->getStore($storeId)->roundPrice($value);
}
}
$value = preg_replace("#\s+#siu", ' ', trim(strip_tags($value)));
return $value;
}
/**
* Save Product index
*
* @param int $productId
* @param int $storeId
* @param string $index
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
protected function _saveProductIndex($productId, $storeId, $index)
{
if ($this->_engine) {
$this->_engine->saveEntityIndex($productId, $storeId, $index);
}
return $this;
}
/**
* Save Multiply Product indexes
*
* @param int $storeId
* @param array $productIndexes
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
protected function _saveProductIndexes($storeId, $productIndexes)
{
if ($this->_engine) {
$this->_engine->saveEntityIndexes($storeId, $productIndexes);
}
return $this;
}
/**
* Retrieve Date value for store
*
* @param int $storeId
* @param string $date
* @return string
*/
protected function _getStoreDate($storeId, $date = null)
{
if (!isset($this->_dates[$storeId])) {
$timezone = Mage::getStoreConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE, $storeId);
$locale = Mage::getStoreConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_LOCALE, $storeId);
$locale = new Zend_Locale($locale);
$dateObj = new Zend_Date(null, null, $locale);
$dateObj->setTimezone($timezone);
$this->_dates[$storeId] = array($dateObj, $locale->getTranslation(null, 'date', $locale));
}
if (!is_empty_date($date)) {
list($dateObj, $format) = $this->_dates[$storeId];
$dateObj->setDate($date, Varien_Date::DATETIME_INTERNAL_FORMAT);
return $dateObj->toString($format);
}
return null;
}
// Deprecated methods
/**
* Set whether table changes are allowed
*
* @deprecated after 1.6.1.0
* @param bool $value
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
public function setAllowTableChanges($value = true)
{
$this->_allowTableChanges = $value;
return $this;
}
/**
* Update category products indexes
*
* deprecated after 1.6.2.0
*
* @param array $productIds
* @param array $categoryIds
* @return Mage_CatalogSearch_Model_Resource_Fulltext
*/
public function updateCategoryIndex($productIds, $categoryIds)
{
return $this;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment