Skip to content

Instantly share code, notes, and snippets.

@0xPr0xy
Last active August 29, 2015 14:22
Show Gist options
  • Save 0xPr0xy/309d3985aa5e71e1caa3 to your computer and use it in GitHub Desktop.
Save 0xPr0xy/309d3985aa5e71e1caa3 to your computer and use it in GitHub Desktop.
NodePagesConfiguration.php
<?php
namespace Kunstmaan\NodeSearchBundle\Configuration;
use Doctrine\ORM\EntityManager;
use Kunstmaan\AdminBundle\Helper\Security\Acl\Permission\MaskBuilder;
use Kunstmaan\NodeBundle\Entity\NodeVersion;
use Kunstmaan\NodeSearchBundle\Helper\IndexablePagePartsService;
use Kunstmaan\NodeSearchBundle\Helper\SearchBoostInterface;
use Kunstmaan\NodeSearchBundle\Helper\SearchViewTemplateInterface;
use Kunstmaan\NodeBundle\Entity\HasNodeInterface;
use Kunstmaan\NodeBundle\Entity\Node;
use Kunstmaan\NodeBundle\Entity\NodeTranslation;
use Kunstmaan\NodeSearchBundle\Helper\SearchTypeInterface;
use Kunstmaan\PagePartBundle\Helper\HasPagePartsInterface;
use Kunstmaan\SearchBundle\Configuration\SearchConfigurationInterface;
use Kunstmaan\SearchBundle\Helper\IndexableInterface;
use Kunstmaan\SearchBundle\Provider\SearchProviderInterface;
use Kunstmaan\SearchBundle\Search\AnalysisFactoryInterface;
use Kunstmaan\UtilitiesBundle\Helper\ClassLookup;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Security\Acl\Domain\ObjectIdentity;
use Symfony\Component\Security\Acl\Domain\RoleSecurityIdentity;
use Symfony\Component\Security\Acl\Exception\AclNotFoundException;
use Symfony\Component\Security\Acl\Model\AclInterface;
use Symfony\Component\Security\Acl\Model\AclProviderInterface;
use Symfony\Component\Security\Acl\Model\AuditableEntryInterface;
class NodePagesConfiguration implements SearchConfigurationInterface
{
/** @var string */
private $indexName;
/** @var string */
private $indexType;
/** @var SearchProviderInterface */
private $searchProvider;
/** @var array */
private $locales = array();
/** @var array */
private $analyzerLanguages;
/** @var EntityManager */
private $em;
/** @var array */
private $documents = array();
/** @var ContainerInterface */
private $container;
/** @var AclProviderInterface */
private $aclProvider = null;
/** @var LoggerInterface */
private $logger = null;
/** @var IndexablePagePartsService */
private $indexablePagePartsService;
/**
* @param ContainerInterface $container
* @param SearchProviderInterface $searchProvider
* @param string $name
* @param string $type
*/
public function __construct($container, $searchProvider, $name, $type)
{
$this->container = $container;
$this->indexName = $name;
$this->indexType = $type;
$this->searchProvider = $searchProvider;
$this->locales = explode('|', $this->container->getParameter('requiredlocales'));
$this->analyzerLanguages = $this->container->getParameter('analyzer_languages');
$this->em = $this->container->get('doctrine')->getManager();
}
/**
* @param AclProviderInterface $aclProvider
*/
public function setAclProvider(AclProviderInterface $aclProvider)
{
$this->aclProvider = $aclProvider;
}
/**
* @param IndexablePagePartsService $indexablePagePartsService
*/
public function setIndexablePagePartsService(IndexablePagePartsService $indexablePagePartsService)
{
$this->indexablePagePartsService = $indexablePagePartsService;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* Create node index
*/
public function createIndex()
{
//build new index
$index = $this->searchProvider->createIndex($this->indexName);
//create analysis
$analysis = $this->container->get('kunstmaan_search.search.factory.analysis');
foreach ($this->locales as $locale) {
$analysis
->addIndexAnalyzer($locale)
->addSuggestionAnalyzer($locale);
}
//create index with analysis
$this->setAnalysis($index, $analysis);
//create mapping
foreach ($this->locales as $locale) {
$this->setMapping($index, $locale);
}
}
/**
* Populate node index
*/
public function populateIndex()
{
$nodeRepository = $this->em->getRepository('KunstmaanNodeBundle:Node');
$nodes = $nodeRepository->getAllTopNodes();
foreach ($nodes as $node) {
foreach ($this->locales as $lang) {
$this->createNodeDocuments($node, $lang);
}
}
if (!empty($this->documents)) {
$this->searchProvider->addDocuments($this->documents);
$this->documents = array();
}
}
/**
* Index a node (including its children) - for the specified language only
*
* @param Node $node
* @param string $lang
*/
public function indexNode(Node $node, $lang)
{
$this->createNodeDocuments($node, $lang);
if (!empty($this->documents)) {
$this->searchProvider->addDocuments($this->documents);
$this->documents = array();
}
}
/**
* Add documents for the node translation (and children) to the index
*
* @param Node $node
* @param string $lang
*/
public function createNodeDocuments(Node $node, $lang)
{
$nodeTranslation = $node->getNodeTranslation($lang);
if ($nodeTranslation) {
if ($this->indexNodeTranslation($nodeTranslation)) {
$this->indexChildren($node, $lang);
}
}
}
/**
* Index all children of the specified node (only for the specified language)
*
* @param Node $node
* @param string $lang
*/
public function indexChildren(Node $node, $lang)
{
foreach ($node->getChildren() as $childNode) {
$this->indexNode($childNode, $lang);
}
}
/**
* Index a node translation
*
* @param NodeTranslation $nodeTranslation
* @param bool $add Add node immediately to index?
*
* @return bool Return true if the document has been indexed
*/
public function indexNodeTranslation(NodeTranslation $nodeTranslation, $add = false)
{
// Only index online NodeTranslations
if (!$nodeTranslation->isOnline()) {
return false;
}
// Retrieve the public NodeVersion
$publicNodeVersion = $nodeTranslation->getPublicNodeVersion();
if (is_null($publicNodeVersion)) {
return false;
}
$node = $nodeTranslation->getNode();
// Retrieve the referenced entity from the public NodeVersion
$page = $publicNodeVersion->getRef($this->em);
if(!$page instanceof HasNodeInterface){
return false;
}
if ($this->isIndexable($page)) {
$this->addPageToIndex($nodeTranslation, $node, $publicNodeVersion, $page);
if ($add) {
$this->searchProvider->addDocuments($this->documents);
$this->documents = array();
}
}
return true; // return true even if the page itself should not be indexed. This makes sure its children are being processed (i.e. structured nodes)
}
/**
* Return if the page is indexable - by default all pages are indexable, you can override this by implementing
* the IndexableInterface on your page entity and returning false in the isIndexable method.
*
* @param HasNodeInterface $page
*
* @return boolean
*/
protected function isIndexable(HasNodeInterface $page)
{
// If the page doesn't implement IndexableInterface interface or it returns true on isIndexable, index the page
return (!($page instanceof IndexableInterface) || $page->isIndexable());
}
/**
* Remove the specified node translation from the index
*
* @param NodeTranslation $nodeTranslation
*/
public function deleteNodeTranslation(NodeTranslation $nodeTranslation)
{
$uid = 'nodetranslation_' . $nodeTranslation->getId();
$this->searchProvider->deleteDocument($this->indexName, $this->indexType, $uid);
}
/**
* Delete the specified index
*/
public function deleteIndex()
{
$this->searchProvider->deleteIndex($this->indexName);
}
/**
* Apply the analysis factory to the index
*
* @param \Elastica\Index $index
* @param AnalysisFactoryInterface $analysis
*/
public function setAnalysis(\Elastica\Index $index, AnalysisFactoryInterface $analysis)
{
$index->create(
array(
'number_of_shards' => 4,
'number_of_replicas' => 1,
'analysis' => $analysis->build()
)
);
}
/**
* Return default search fields mapping for node translations
*
* @param \Elastica\Index $index
* @param string $lang
*
* @return \Elastica\Type\Mapping
*/
protected function getMapping(\Elastica\Index $index, $lang = 'en')
{
$mapping = new \Elastica\Type\Mapping();
$mapping->setType($index->getType($this->indexType . '_' . $lang));
$mapping->setParam('analyzer', 'index_analyzer_' . $lang);
$mapping->setParam('_boost', array('name' => '_boost', 'null_value' => 1.0));
$mapping->setProperties(
array(
'node_id' => array(
'type' => 'integer',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'nodetranslation_id' => array(
'type' => 'integer',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'nodeversion_id' => array(
'type' => 'integer',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'title' => array(
'type' => 'string',
'include_in_all' => true
),
'lang' => array(
'type' => 'string',
'include_in_all' => true,
'index' => 'not_analyzed'
),
'slug' => array(
'type' => 'string',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'type' => array(
'type' => 'string',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'page_class' => array(
'type' => 'string',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'contentanalyzer' => array(
'type' => 'string',
'include_in_all' => true,
'index' => 'not_analyzed'
),
'content' => array(
'type' => 'string',
'include_in_all' => true
),
'created' => array(
'type' => 'date',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'updated' => array(
'type' => 'date',
'include_in_all' => false,
'index' => 'not_analyzed'
),
'view_roles' => array(
'type' => 'string',
'include_in_all' => true,
'index' => 'not_analyzed',
'index_name' => 'view_role'
),
'_boost' => array(
'type' => 'float',
'include_in_all' => false
)
)
);
return $mapping;
}
/**
* Initialize the index with the default search fields mapping
*
* @param \Elastica\Index $index
* @param string $lang
*/
protected function setMapping(\Elastica\Index $index, $lang = 'en')
{
$mapping = $this->getMapping($index, $lang);
$mapping->send();
$index->refresh();
}
protected function getTrainingEntities()
{
$entityRepository = $this->em->getRepository('HumaneEndpointsTrainingBundle:Training');
$trainingEntities = $entityRepository->findAll();
return $trainingEntities;
}
/**
* Create a search document for a page
*
* @param NodeTranslation $nodeTranslation
* @param Node $node
* @param NodeVersion $publicNodeVersion
* @param HasNodeInterface $page
*/
protected function addPageToIndex(
NodeTranslation $nodeTranslation,
Node $node,
NodeVersion $publicNodeVersion,
HasNodeInterface $page
) {
$doc = array(
'node_id' => $node->getId(),
'node_translation_id' => $nodeTranslation->getId(),
'node_version_id' => $publicNodeVersion->getId(),
'title' => $nodeTranslation->getTitle(),
'lang' => $nodeTranslation->getLang(),
'slug' => $nodeTranslation->getFullSlug(),
'page_class' => ClassLookup::getClass($page),
'created' => $this->getUTCDateTime($nodeTranslation->getCreated())->format(\DateTime::ISO8601),
'updated' => $this->getUTCDateTime($nodeTranslation->getUpdated())->format(\DateTime::ISO8601)
);
if ($this->logger) {
$this->logger->info('Indexing document : ' . implode(', ', $doc));
}
// Permissions
$this->addPermissions($node, $doc);
// Search type
$this->addSearchType($page, $doc);
// Analyzer field
$this->addAnalyzer($nodeTranslation, $doc);
// Parent and Ancestors
$this->addParentAndAncestors($node, $doc);
// Content
$this->addPageContent($nodeTranslation, $page, $doc);
// Add document to index
$uid = 'nodetranslation_' . $nodeTranslation->getId();
$this->addBoost($node, $page, $doc);
$this->addCustomData($node, $page, $doc);
$this->documents[] = $this->searchProvider->createDocument(
$uid,
$doc,
$this->indexName,
$this->indexType . '_' . $nodeTranslation->getLang()
);
}
/**
* Add view permissions to the index document
*
* @param Node $node
* @param array $doc
*
* @return array
*/
protected function addPermissions(Node $node, &$doc)
{
$roles = array();
if (!is_null($this->aclProvider)) {
$roles = $this->getAclPermissions($node);
} else {
// Fallback when no ACL available / assume everything is accessible...
$roles = array('IS_AUTHENTICATED_ANONYMOUSLY');
}
$doc['view_roles'] = $roles;
}
/**
* Add type to the index document
*
* @param object $page
* @param array $doc
*
* @return array
*/
protected function addSearchType($page, &$doc)
{
// Type
$type = ClassLookup::getClassName($page);
if ($page instanceof SearchTypeInterface) {
$type = $page->getSearchType();
}
$doc['type'] = $type;
}
/**
* Add content analyzer to the index document
*
* @param NodeTranslation $nodeTranslation
* @param array $doc
*
* @return array
*/
protected function addAnalyzer(NodeTranslation $nodeTranslation, &$doc)
{
$language = $this->analyzerLanguages[$nodeTranslation->getLang()]['analyzer'];
$doc['contentanalyzer'] = $language;
}
/**
* Add parent nodes to the index document
*
* @param Node $node
* @param array $doc
*
* @return array
*/
protected function addParentAndAncestors($node, &$doc)
{
$parent = $node->getParent();
$parentNodeTranslation = null;
if ($parent) {
$doc['parent'] = $parent->getId();
$ancestors = array();
do {
$ancestors[] = $parent->getId();
$parent = $parent->getParent();
} while ($parent);
$doc['ancestors'] = $ancestors;
}
}
/**
* Add page content to the index document
*
* @param NodeTranslation $nodeTranslation
* @param HasNodeInterface $page
* @param array $doc
*
* @return array
*/
protected function addPageContent(NodeTranslation $nodeTranslation, $page, &$doc)
{
$this->enterRequestScope($nodeTranslation->getLang());
if ($this->logger) {
$this->logger->debug(
sprintf(
'Indexing page "%s" / lang : %s / type : %s / id : %d / node id : %d',
$page->getTitle(),
$nodeTranslation->getLang(),
get_class($page),
$page->getId(),
$nodeTranslation->getNode()->getId()
)
);
}
$renderer = $this->container->get('templating');
$doc['content'] = '';
if ($page instanceof SearchViewTemplateInterface) {
$doc['content'] = $this->renderCustomSearchView($nodeTranslation, $page, $renderer);
return;
}
if ($page instanceof HasPagePartsInterface) {
$doc['content'] = $this->renderDefaultSearchView($nodeTranslation, $page, $renderer);
return;
}
}
/**
* Enter request scope if it is not active yet...
*
* @param string $lang
*/
protected function enterRequestScope($lang)
{
if (!$this->container->isScopeActive('request')) {
$this->container->enterScope('request');
$request = new Request();
$request->setLocale($lang);
$major = Kernel::MAJOR_VERSION;
$minor = Kernel::MINOR_VERSION;
if ((int)$major > 2 || ((int)$major == 2 && (int)$minor >= 4)) {
$requestStack = $this->container->get('request_stack');
$requestStack->push($request);
}
$this->container->set('request', $request, 'request');
}
}
/**
* Render a custom search view
*
* @param NodeTranslation $nodeTranslation
* @param $page
* @param $renderer
*
* @return string
*/
protected function renderCustomSearchView(NodeTranslation $nodeTranslation, $page, $renderer)
{
$view = $page->getSearchView();
$content = $this->removeHtml(
$renderer->render(
$view,
array(
'locale' => $nodeTranslation->getLang(),
'page' => $page,
'indexMode' => true
)
)
);
return $content;
}
/**
* Render default search view (all indexable pageparts in the main context of the page)
*
* @param NodeTranslation $nodeTranslation
* @param $page
* @param $renderer
*
* @return string
*/
protected function renderDefaultSearchView(NodeTranslation $nodeTranslation, $page, $renderer)
{
$pageparts = $this->indexablePagePartsService->getIndexablePageParts($page);
$view = 'KunstmaanNodeSearchBundle:PagePart:view.html.twig';
$content = $this->removeHtml(
$renderer->render(
$view,
array(
'locale' => $nodeTranslation->getLang(),
'page' => $page,
'pageparts' => $pageparts,
'indexMode' => true
)
)
);
return $content;
}
/**
* Add boost to the index document
*
* @param Node $node
* @param HasNodeInterface $page
* @param array $doc
*/
protected function addBoost($node, HasNodeInterface $page, &$doc)
{
// Check page type boost
$doc['_boost'] = 1.0;
if ($page instanceof SearchBoostInterface) {
$doc['_boost'] += $page->getSearchBoost();
}
// Check if page is boosted
$nodeSearch = $this->em->getRepository('KunstmaanNodeSearchBundle:NodeSearch')
->findOneByNode($node);
if ($nodeSearch !== null) {
$doc['_boost'] += $nodeSearch->getBoost();
}
}
/**
* Add custom data to index document (you can override to add custom fields to the search index)
*
* @param HasNodeInterface $page
* @param array $doc
*/
protected function addCustomData($node, HasNodeInterface $page, &$doc)
{
if($node->getInternalName() == 'training-module'){
$entities = $this->getTrainingEntities();
$trainingTitles = array();
foreach($entities as $training){
$trainingTitles[] = $training->getTitle();
}
$doc['entities'] = $trainingTitles;
}
// You can add custom data to be added to the document index array ($doc) here if you inherit from this class...
}
/**
* Convert a DateTime to UTC equivalent...
*
* @param \DateTime $dateTime
*
* @return \DateTime
*/
private function getUTCDateTime(\DateTime $dateTime)
{
$result = clone $dateTime;
$result->setTimezone(new \DateTimeZone('UTC'));
return $result;
}
/**
* Removes all HTML markup & decode HTML entities
*/
protected function removeHtml($text)
{
// Remove HTML markup
$result = strip_tags($text);
// Decode HTML entities
$result = trim(html_entity_decode($result, ENT_QUOTES));
return $result;
}
/**
* Fetch ACL permissions for the specified entity
*
* @param object $object
*
* @return array
*/
protected function getAclPermissions($object)
{
$roles = array();
try {
$objectIdentity = ObjectIdentity::fromDomainObject($object);
/* @var AclInterface $acl */
$acl = $this->aclProvider->findAcl($objectIdentity);
$objectAces = $acl->getObjectAces();
/* @var AuditableEntryInterface $ace */
foreach ($objectAces as $ace) {
$securityIdentity = $ace->getSecurityIdentity();
if (
$securityIdentity instanceof RoleSecurityIdentity &&
($ace->getMask() & MaskBuilder::MASK_VIEW != 0)
) {
$roles[] = $securityIdentity->getRole();
}
}
} catch (AclNotFoundException $e) {
// No ACL found... assume default
$roles = array('IS_AUTHENTICATED_ANONYMOUSLY');
}
return $roles;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment