Skip to content

[5.0] Smart Search: Add debugging features #2827

@jgerman-bot

Description

@jgerman-bot

New language relevant PR in upstream repo: joomla/joomla-cms#36753 Here are the upstream changes:

Click to expand the diff!
diff --git a/administrator/components/com_finder/forms/indexer.xml b/administrator/components/com_finder/forms/indexer.xml
new file mode 100644
index 000000000000..a9559102a0c4
--- /dev/null
+++ b/administrator/components/com_finder/forms/indexer.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<form>
+	<fieldset name="form">
+		<field
+			name="plugin"
+			type="plugins"
+			label="COM_FINDER_FIELD_FINDER_PLUGIN_LABEL"
+			folder="finder"
+			required="true"
+		/>
+
+		<field
+			name="id"
+			type="text"
+			label="JGLOBAL_FIELD_ID_LABEL"
+			required="true"
+		/>
+	</fieldset>
+</form>
diff --git a/administrator/components/com_finder/src/Controller/IndexerController.php b/administrator/components/com_finder/src/Controller/IndexerController.php
index 509750020562..51a398129a6f 100644
--- a/administrator/components/com_finder/src/Controller/IndexerController.php
+++ b/administrator/components/com_finder/src/Controller/IndexerController.php
@@ -17,6 +17,9 @@
 use Joomla\CMS\MVC\Controller\BaseController;
 use Joomla\CMS\Plugin\PluginHelper;
 use Joomla\CMS\Session\Session;
+use Joomla\Component\Finder\Administrator\Indexer\Adapter;
+use Joomla\Component\Finder\Administrator\Indexer\DebugAdapter;
+use Joomla\Component\Finder\Administrator\Indexer\DebugIndexer;
 use Joomla\Component\Finder\Administrator\Indexer\Indexer;
 use Joomla\Component\Finder\Administrator\Response\Response;
 
@@ -147,22 +150,6 @@ public function batch()
         // Import the finder plugins.
         PluginHelper::importPlugin('finder');
 
-        /*
-         * We are going to swap out the raw document object with an HTML document
-         * in order to work around some plugins that don't do proper environment
-         * checks before trying to use HTML document functions.
-         */
-        $lang = Factory::getLanguage();
-
-        // Get the document properties.
-        $attributes = [
-            'charset'   => 'utf-8',
-            'lineend'   => 'unix',
-            'tab'       => '  ',
-            'language'  => $lang->getTag(),
-            'direction' => $lang->isRtl() ? 'rtl' : 'ltr',
-        ];
-
         // Start the indexer.
         try {
             // Trigger the onBeforeIndex event.
@@ -281,4 +268,112 @@ public static function sendResponse($data = null)
         // Send the JSON response.
         echo json_encode($response);
     }
+
+    /**
+     * Method to call a specific indexing plugin and return debug info
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     * @internal
+     */
+    public function debug()
+    {
+        // Check for a valid token. If invalid, send a 403 with the error message.
+        if (!Session::checkToken('request')) {
+            static::sendResponse(new \Exception(Text::_('JINVALID_TOKEN_NOTICE'), 403));
+
+            return;
+        }
+
+        // We don't want this form to be cached.
+        $this->app->allowCache(false);
+
+        // Put in a buffer to silence noise.
+        ob_start();
+
+        // Remove the script time limit.
+        @set_time_limit(0);
+
+        // Get the indexer state.
+        Indexer::resetState();
+        $state = Indexer::getState();
+
+        // Reset the batch offset.
+        $state->batchOffset = 0;
+
+        // Update the indexer state.
+        Indexer::setState($state);
+
+        // Start the indexer.
+        try {
+            // Import the finder plugins.
+            class_alias(DebugAdapter::class, Adapter::class);
+            $plugin = Factory::getApplication()->bootPlugin($this->app->input->get('plugin'), 'finder');
+            $plugin->setIndexer(new DebugIndexer());
+            $plugin->debug($this->app->input->get('id'));
+
+            $output = '';
+
+            // Create list of attributes
+            $output .= '<fieldset><legend>' . Text::_('COM_FINDER_INDEXER_FIELDSET_ATTRIBUTES') . '</legend>';
+            $output .= '<dl class="row">';
+
+            foreach (DebugIndexer::$item as $key => $value) {
+                $output .= '<dt class="col-sm-2">' . $key . '</dt><dd class="col-sm-10">' . $value . '</dd>';
+            }
+
+            $output .= '</dl>';
+            $output .= '</fieldset>';
+
+            $output .= '<fieldset><legend>' . Text::_('COM_FINDER_INDEXER_FIELDSET_ELEMENTS') . '</legend>';
+            $output .= '<dl class="row">';
+
+            foreach (DebugIndexer::$item->getElements() as $key => $element) {
+                $output .= '<dt class="col-sm-2">' . $key . '</dt><dd class="col-sm-10">' . $element . '</dd>';
+            }
+
+            $output .= '</dl>';
+            $output .= '</fieldset>';
+
+            $output .= '<fieldset><legend>' . Text::_('COM_FINDER_INDEXER_FIELDSET_INSTRUCTIONS') . '</legend>';
+            $output .= '<dl class="row">';
+            $contexts = [
+                1 => 'Title context',
+                2 => 'Text context',
+                3 => 'Meta context',
+                4 => 'Path context',
+                5 => 'Misc context',
+            ];
+
+            foreach (DebugIndexer::$item->getInstructions() as $key => $element) {
+                $output .= '<dt class="col-sm-2">' . $contexts[$key] . '</dt><dd class="col-sm-10">' . json_encode($element) . '</dd>';
+            }
+
+            $output .= '</dl>';
+            $output .= '</fieldset>';
+
+            $output .= '<fieldset><legend>' . Text::_('COM_FINDER_INDEXER_FIELDSET_TAXONOMIES') . '</legend>';
+            $output .= '<dl class="row">';
+
+            foreach (DebugIndexer::$item->getTaxonomy() as $key => $element) {
+                $output .= '<dt class="col-sm-2">' . $key . '</dt><dd class="col-sm-10">' . json_encode($element) . '</dd>';
+            }
+
+            $output .= '</dl>';
+            $output .= '</fieldset>';
+
+            // Get the indexer state.
+            $state           = Indexer::getState();
+            $state->start    = 0;
+            $state->complete = 0;
+            $state->rendered = $output;
+
+            echo json_encode($state);
+        } catch (\Exception $e) {
+            // Catch an exception and return the response.
+            // Send the response.
+            static::sendResponse($e);
+        }
+    }
 }
diff --git a/administrator/components/com_finder/src/Indexer/DebugAdapter.php b/administrator/components/com_finder/src/Indexer/DebugAdapter.php
new file mode 100644
index 000000000000..3e0ffa74b5e3
--- /dev/null
+++ b/administrator/components/com_finder/src/Indexer/DebugAdapter.php
@@ -0,0 +1,952 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Finder\Administrator\Indexer;
+
+use Exception;
+use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\CMS\Table\Table;
+use Joomla\Database\DatabaseInterface;
+use Joomla\Database\QueryInterface;
+use Joomla\Utilities\ArrayHelper;
+
+/**
+ * Prototype debug adapter class for the Finder indexer package.
+ * THIS CLASS IS ONLY TO BE USED FOR DEBUGGING PURPOSES! DON'T
+ * USE IT FOR PRODUCTIVE USE!
+ *
+ * @since  __DEPLOY_VERSION__
+ * @internal
+ */
+abstract class DebugAdapter extends CMSPlugin
+{
+    /**
+     * The context is somewhat arbitrary but it must be unique or there will be
+     * conflicts when managing plugin/indexer state. A good best practice is to
+     * use the plugin name suffix as the context. For example, if the plugin is
+     * named 'plgFinderContent', the context could be 'Content'.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $context;
+
+    /**
+     * The extension name.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $extension;
+
+    /**
+     * The sublayout to use when rendering the results.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $layout;
+
+    /**
+     * The mime type of the content the adapter indexes.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $mime;
+
+    /**
+     * The access level of an item before save.
+     *
+     * @var    integer
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $old_access;
+
+    /**
+     * The access level of a category before save.
+     *
+     * @var    integer
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $old_cataccess;
+
+    /**
+     * The type of content the adapter indexes.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $type_title;
+
+    /**
+     * The type id of the content.
+     *
+     * @var    integer
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $type_id;
+
+    /**
+     * The database object.
+     *
+     * @var    DatabaseInterface
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $db;
+
+    /**
+     * The table name.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $table;
+
+    /**
+     * The indexer object.
+     *
+     * @var    Indexer
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $indexer;
+
+    /**
+     * The field the published state is stored in.
+     *
+     * @var    string
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $state_field = 'state';
+
+    /**
+     * Method to instantiate the indexer adapter.
+     *
+     * @param   object  $subject  The object to observe.
+     * @param   array   $config   An array that holds the plugin configuration.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function __construct(&$subject, $config)
+    {
+        // Call the parent constructor.
+        parent::__construct($subject, $config);
+
+        // Get the type id.
+        $this->type_id = $this->getTypeId();
+
+        // Add the content type if it doesn't exist and is set.
+        if (empty($this->type_id) && !empty($this->type_title)) {
+            $this->type_id = Helper::addContentType($this->type_title, $this->mime);
+        }
+
+        // Check for a layout override.
+        if ($this->params->get('layout')) {
+            $this->layout = $this->params->get('layout');
+        }
+
+        // Get the indexer object
+        $this->indexer = new Indexer($this->db);
+    }
+
+    /**
+     * Method to get the adapter state and push it into the indexer.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on error.
+     */
+    public function onStartIndex()
+    {
+        // Get the indexer state.
+        $iState = Indexer::getState();
+
+        // Get the number of content items.
+        $total = (int) $this->getContentCount();
+
+        // Add the content count to the total number of items.
+        $iState->totalItems += $total;
+
+        // Populate the indexer state information for the adapter.
+        $iState->pluginState[$this->context]['total']  = $total;
+        $iState->pluginState[$this->context]['offset'] = 0;
+
+        // Set the indexer state.
+        Indexer::setState($iState);
+    }
+
+    /**
+     * Method to prepare for the indexer to be run. This method will often
+     * be used to include dependencies and things of that nature.
+     *
+     * @return  boolean  True on success.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on error.
+     */
+    public function onBeforeIndex()
+    {
+        // Get the indexer and adapter state.
+        $iState = Indexer::getState();
+        $aState = $iState->pluginState[$this->context];
+
+        // Check the progress of the indexer and the adapter.
+        if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) {
+            return true;
+        }
+
+        // Run the setup method.
+        return $this->setup();
+    }
+
+    /**
+     * Method to index a batch of content items. This method can be called by
+     * the indexer many times throughout the indexing process depending on how
+     * much content is available for indexing. It is important to track the
+     * progress correctly so we can display it to the user.
+     *
+     * @return  boolean  True on success.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on error.
+     */
+    public function onBuildIndex()
+    {
+        // Get the indexer and adapter state.
+        $iState = Indexer::getState();
+        $aState = $iState->pluginState[$this->context];
+
+        // Check the progress of the indexer and the adapter.
+        if ($iState->batchOffset == $iState->batchSize || $aState['offset'] == $aState['total']) {
+            return true;
+        }
+
+        // Get the batch offset and size.
+        $offset = (int) $aState['offset'];
+        $limit  = (int) ($iState->batchSize - $iState->batchOffset);
+
+        // Get the content items to index.
+        $items = $this->getItems($offset, $limit);
+
+        // Iterate through the items and index them.
+        for ($i = 0, $n = count($items); $i < $n; $i++) {
+            // Index the item.
+            $this->index($items[$i]);
+
+            // Adjust the offsets.
+            $offset++;
+            $iState->batchOffset++;
+            $iState->totalItems--;
+        }
+
+        // Update the indexer state.
+        $aState['offset']                    = $offset;
+        $iState->pluginState[$this->context] = $aState;
+        Indexer::setState($iState);
+
+        return true;
+    }
+
+    /**
+     * Method to remove outdated index entries
+     *
+     * @return  integer
+     *
+     * @since   ___DEPLOY_VERSION__
+     */
+    public function onFinderGarbageCollection()
+    {
+        $db      = $this->db;
+        $type_id = $this->getTypeId();
+
+        $query    = $db->getQuery(true);
+        $subquery = $db->getQuery(true);
+        $subquery->select('CONCAT(' . $db->quote($this->getUrl('', $this->extension, $this->layout)) . ', id)')
+            ->from($db->quoteName($this->table));
+        $query->select($db->quoteName('l.link_id'))
+            ->from($db->quoteName('#__finder_links', 'l'))
+            ->where($db->quoteName('l.type_id') . ' = ' . $type_id)
+            ->where($db->quoteName('l.url') . ' LIKE ' . $db->quote($this->getUrl('%', $this->extension, $this->layout)))
+            ->where($db->quoteName('l.url') . ' NOT IN (' . $subquery . ')');
+        $db->setQuery($query);
+        $items = $db->loadColumn();
+
+        foreach ($items as $item) {
+            $this->indexer->remove($item);
+        }
+
+        return count($items);
+    }
+
+    /**
+     * Method to change the value of a content item's property in the links
+     * table. This is used to synchronize published and access states that
+     * are changed when not editing an item directly.
+     *
+     * @param   string   $id        The ID of the item to change.
+     * @param   string   $property  The property that is being changed.
+     * @param   integer  $value     The new value of that property.
+     *
+     * @return  boolean  True on success.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function change($id, $property, $value)
+    {
+        // Check for a property we know how to handle.
+        if ($property !== 'state' && $property !== 'access') {
+            return true;
+        }
+
+        // Get the URL for the content id.
+        $item = $this->db->quote($this->getUrl($id, $this->extension, $this->layout));
+
+        // Update the content items.
+        $query = $this->db->getQuery(true)
+            ->update($this->db->quoteName('#__finder_links'))
+            ->set($this->db->quoteName($property) . ' = ' . (int) $value)
+            ->where($this->db->quoteName('url') . ' = ' . $item);
+        $this->db->setQuery($query);
+        $this->db->execute();
+
+        return true;
+    }
+
+    /**
+     * Method to index an item.
+     *
+     * @param   Result  $item  The item to index as a Result object.
+     *
+     * @return  boolean  True on success.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    abstract protected function index(Result $item);
+
+    /**
+     * Method to reindex an item.
+     *
+     * @param   integer  $id  The ID of the item to reindex.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function reindex($id)
+    {
+        // Run the setup method.
+        $this->setup();
+
+        // Remove the old item.
+        $this->remove($id, false);
+
+        // Get the item.
+        $item = $this->getItem($id);
+
+        // Index the item.
+        $this->index($item);
+
+        Taxonomy::removeOrphanNodes();
+    }
+
+    /**
+     * Method to remove an item from the index.
+     *
+     * @param   string  $id                The ID of the item to remove.
+     * @param   bool    $removeTaxonomies  Remove empty taxonomies
+     *
+     * @return  boolean  True on success.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function remove($id, $removeTaxonomies = true)
+    {
+        // Get the item's URL
+        $url = $this->db->quote($this->getUrl($id, $this->extension, $this->layout));
+
+        // Get the link ids for the content items.
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('link_id'))
+            ->from($this->db->quoteName('#__finder_links'))
+            ->where($this->db->quoteName('url') . ' = ' . $url);
+        $this->db->setQuery($query);
+        $items = $this->db->loadColumn();
+
+        // Check the items.
+        if (empty($items)) {
+            $this->getApplication()->triggerEvent('onFinderIndexAfterDelete', [$id]);
+
+            return true;
+        }
+
+        // Remove the items.
+        foreach ($items as $item) {
+            $this->indexer->remove($item, $removeTaxonomies);
+        }
+
+        return true;
+    }
+
+    /**
+     * Method to setup the adapter before indexing.
+     *
+     * @return  boolean  True on success, false on failure.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    abstract protected function setup();
+
+    /**
+     * Method to update index data on category access level changes
+     *
+     * @param   Table  $row  A Table object
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function categoryAccessChange($row)
+    {
+        $query = clone $this->getStateQuery();
+        $query->where('c.id = ' . (int) $row->id);
+
+        // Get the access level.
+        $this->db->setQuery($query);
+        $items = $this->db->loadObjectList();
+
+        // Adjust the access level for each item within the category.
+        foreach ($items as $item) {
+            // Set the access level.
+            $temp = max($item->access, $row->access);
+
+            // Update the item.
+            $this->change((int) $item->id, 'access', $temp);
+        }
+    }
+
+    /**
+     * Method to update index data on category access level changes
+     *
+     * @param   array    $pks    A list of primary key ids of the content that has changed state.
+     * @param   integer  $value  The value of the state that the content has been changed to.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function categoryStateChange($pks, $value)
+    {
+        /*
+         * The item's published state is tied to the category
+         * published state so we need to look up all published states
+         * before we change anything.
+         */
+        foreach ($pks as $pk) {
+            $query = clone $this->getStateQuery();
+            $query->where('c.id = ' . (int) $pk);
+
+            // Get the published states.
+            $this->db->setQuery($query);
+            $items = $this->db->loadObjectList();
+
+            // Adjust the state for each item within the category.
+            foreach ($items as $item) {
+                // Translate the state.
+                $temp = $this->translateState($item->state, $value);
+
+                // Update the item.
+                $this->change($item->id, 'state', $temp);
+            }
+        }
+    }
+
+    /**
+     * Method to check the existing access level for categories
+     *
+     * @param   Table  $row  A Table object
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function checkCategoryAccess($row)
+    {
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('access'))
+            ->from($this->db->quoteName('#__categories'))
+            ->where($this->db->quoteName('id') . ' = ' . (int) $row->id);
+        $this->db->setQuery($query);
+
+        // Store the access level to determine if it changes
+        $this->old_cataccess = $this->db->loadResult();
+    }
+
+    /**
+     * Method to check the existing access level for items
+     *
+     * @param   Table  $row  A Table object
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function checkItemAccess($row)
+    {
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('access'))
+            ->from($this->db->quoteName($this->table))
+            ->where($this->db->quoteName('id') . ' = ' . (int) $row->id);
+        $this->db->setQuery($query);
+
+        // Store the access level to determine if it changes
+        $this->old_access = $this->db->loadResult();
+    }
+
+    /**
+     * Method to get the number of content items available to index.
+     *
+     * @return  integer  The number of content items available to index.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function getContentCount()
+    {
+        $return = 0;
+
+        // Get the list query.
+        $query = $this->getListQuery();
+
+        // Check if the query is valid.
+        if (empty($query)) {
+            return $return;
+        }
+
+        // Tweak the SQL query to make the total lookup faster.
+        if ($query instanceof QueryInterface) {
+            $query = clone $query;
+            $query->clear('select')
+                ->select('COUNT(*)')
+                ->clear('order');
+        }
+
+        // Get the total number of content items to index.
+        $this->db->setQuery($query);
+
+        return (int) $this->db->loadResult();
+    }
+
+    /**
+     * Method to get a content item to index.
+     *
+     * @param   integer  $id  The id of the content item.
+     *
+     * @return  Result  A Result object.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function getItem($id)
+    {
+        // Get the list query and add the extra WHERE clause.
+        $query = $this->getListQuery();
+        $query->where('a.id = ' . (int) $id);
+
+        // Get the item to index.
+        $this->db->setQuery($query);
+        $item = $this->db->loadAssoc();
+
+        // Convert the item to a result object.
+        $item = ArrayHelper::toObject((array) $item, Result::class);
+
+        // Set the item type.
+        $item->type_id = $this->type_id;
+
+        // Set the item layout.
+        $item->layout = $this->layout;
+
+        return $item;
+    }
+
+    /**
+     * Method to get a list of content items to index.
+     *
+     * @param   integer         $offset  The list offset.
+     * @param   integer         $limit   The list limit.
+     * @param   QueryInterface  $query   A QueryInterface object. [optional]
+     *
+     * @return  Result[]  An array of Result objects.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function getItems($offset, $limit, $query = null)
+    {
+        // Get the content items to index.
+        $this->db->setQuery($this->getListQuery($query)->setLimit($limit, $offset));
+        $items = $this->db->loadAssocList();
+
+        foreach ($items as &$item) {
+            $item = ArrayHelper::toObject($item, Result::class);
+
+            // Set the item type.
+            $item->type_id = $this->type_id;
+
+            // Set the mime type.
+            $item->mime = $this->mime;
+
+            // Set the item layout.
+            $item->layout = $this->layout;
+        }
+
+        return $items;
+    }
+
+    /**
+     * Method to get the SQL query used to retrieve the list of content items.
+     *
+     * @param   mixed  $query  A QueryInterface object. [optional]
+     *
+     * @return  QueryInterface  A database object.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getListQuery($query = null)
+    {
+        // Check if we can use the supplied SQL query.
+        return $query instanceof QueryInterface ? $query : $this->db->getQuery(true);
+    }
+
+    /**
+     * Method to get the plugin type
+     *
+     * @param   integer  $id  The plugin ID
+     *
+     * @return  string  The plugin type
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getPluginType($id)
+    {
+        // Prepare the query
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('element'))
+            ->from($this->db->quoteName('#__extensions'))
+            ->where($this->db->quoteName('extension_id') . ' = ' . (int) $id);
+        $this->db->setQuery($query);
+
+        return $this->db->loadResult();
+    }
+
+    /**
+     * Method to get a SQL query to load the published and access states for
+     * an article and category.
+     *
+     * @return  QueryInterface  A database object.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getStateQuery()
+    {
+        $query = $this->db->getQuery(true);
+
+        // Item ID
+        $query->select('a.id');
+
+        // Item and category published state
+        $query->select('a.' . $this->state_field . ' AS state, c.published AS cat_state');
+
+        // Item and category access levels
+        $query->select('a.access, c.access AS cat_access')
+            ->from($this->table . ' AS a')
+            ->join('LEFT', '#__categories AS c ON c.id = a.catid');
+
+        return $query;
+    }
+
+    /**
+     * Method to get the query clause for getting items to update by time.
+     *
+     * @param   string  $time  The modified timestamp.
+     *
+     * @return  QueryInterface  A database object.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getUpdateQueryByTime($time)
+    {
+        // Build an SQL query based on the modified time.
+        $query = $this->db->getQuery(true)
+            ->where('a.modified >= ' . $this->db->quote($time));
+
+        return $query;
+    }
+
+    /**
+     * Method to get the query clause for getting items to update by id.
+     *
+     * @param   array  $ids  The ids to load.
+     *
+     * @return  QueryInterface  A database object.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getUpdateQueryByIds($ids)
+    {
+        // Build an SQL query based on the item ids.
+        $query = $this->db->getQuery(true)
+            ->where('a.id IN(' . implode(',', $ids) . ')');
+
+        return $query;
+    }
+
+    /**
+     * Method to get the type id for the adapter content.
+     *
+     * @return  integer  The numeric type id for the content.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function getTypeId()
+    {
+        // Get the type id from the database.
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('id'))
+            ->from($this->db->quoteName('#__finder_types'))
+            ->where($this->db->quoteName('title') . ' = ' . $this->db->quote($this->type_title));
+        $this->db->setQuery($query);
+
+        return (int) $this->db->loadResult();
+    }
+
+    /**
+     * Method to get the URL for the item. The URL is how we look up the link
+     * in the Finder index.
+     *
+     * @param   integer  $id         The id of the item.
+     * @param   string   $extension  The extension the category is in.
+     * @param   string   $view       The view for the URL.
+     *
+     * @return  string  The URL of the item.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function getUrl($id, $extension, $view)
+    {
+        return 'index.php?option=' . $extension . '&view=' . $view . '&id=' . $id;
+    }
+
+    /**
+     * Method to get the page title of any menu item that is linked to the
+     * content item, if it exists and is set.
+     *
+     * @param   string  $url  The URL of the item.
+     *
+     * @return  mixed  The title on success, null if not found.
+     *
+     * @since   __DEPLOY_VERSION__
+     * @throws  Exception on database error.
+     */
+    protected function getItemMenuTitle($url)
+    {
+        $return = null;
+
+        // Set variables
+        $user   = $this->getApplication()->getIdentity();
+        $groups = implode(',', $user->getAuthorisedViewLevels());
+
+        // Build a query to get the menu params.
+        $query = $this->db->getQuery(true)
+            ->select($this->db->quoteName('params'))
+            ->from($this->db->quoteName('#__menu'))
+            ->where($this->db->quoteName('link') . ' = ' . $this->db->quote($url))
+            ->where($this->db->quoteName('published') . ' = 1')
+            ->where($this->db->quoteName('access') . ' IN (' . $groups . ')');
+
+        // Get the menu params from the database.
+        $this->db->setQuery($query);
+        $params = $this->db->loadResult();
+
+        // Check the results.
+        if (empty($params)) {
+            return $return;
+        }
+
+        // Instantiate the params.
+        $params = json_decode($params);
+
+        // Get the page title if it is set.
+        if (isset($params->page_title) && $params->page_title) {
+            $return = $params->page_title;
+        }
+
+        return $return;
+    }
+
+    /**
+     * Method to update index data on access level changes
+     *
+     * @param   Table  $row  A Table object
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function itemAccessChange($row)
+    {
+        $query = clone $this->getStateQuery();
+        $query->where('a.id = ' . (int) $row->id);
+
+        // Get the access level.
+        $this->db->setQuery($query);
+        $item = $this->db->loadObject();
+
+        // Set the access level.
+        $temp = max($row->access, $item->cat_access);
+
+        // Update the item.
+        $this->change((int) $row->id, 'access', $temp);
+    }
+
+    /**
+     * Method to update index data on published state changes
+     *
+     * @param   array    $pks    A list of primary key ids of the content that has changed state.
+     * @param   integer  $value  The value of the state that the content has been changed to.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function itemStateChange($pks, $value)
+    {
+        /*
+         * The item's published state is tied to the category
+         * published state so we need to look up all published states
+         * before we change anything.
+         */
+        foreach ($pks as $pk) {
+            $query = clone $this->getStateQuery();
+            $query->where('a.id = ' . (int) $pk);
+
+            // Get the published states.
+            $this->db->setQuery($query);
+            $item = $this->db->loadObject();
+
+            // Translate the state.
+            $temp = $this->translateState($value, $item->cat_state);
+
+            // Update the item.
+            $this->change($pk, 'state', $temp);
+        }
+    }
+
+    /**
+     * Method to update index data when a plugin is disabled
+     *
+     * @param   array  $pks  A list of primary key ids of the content that has changed state.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function pluginDisable($pks)
+    {
+        // Since multiple plugins may be disabled at a time, we need to check first
+        // that we're handling the appropriate one for the context
+        foreach ($pks as $pk) {
+            if ($this->getPluginType($pk) == strtolower($this->context)) {
+                // Get all of the items to unindex them
+                $query = clone $this->getStateQuery();
+                $this->db->setQuery($query);
+                $items = $this->db->loadColumn();
+
+                // Remove each item
+                foreach ($items as $item) {
+                    $this->remove($item);
+                }
+            }
+        }
+    }
+
+    /**
+     * Method to translate the native content states into states that the
+     * indexer can use.
+     *
+     * @param   integer  $item      The item state.
+     * @param   integer  $category  The category state. [optional]
+     *
+     * @return  integer  The translated indexer state.
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function translateState($item, $category = null)
+    {
+        // If category is present, factor in its states as well
+        if ($category !== null && $category == 0) {
+            $item = 0;
+        }
+
+        // Translate the state
+        switch ($item) {
+            // Published and archived items only should return a published state
+            case 1:
+            case 2:
+                return 1;
+
+            // All other states should return an unpublished state
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * Debug method to set the used indexer
+     *
+     * @param   Indexer  $indexer  Indexer object
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function setIndexer(Indexer $indexer)
+    {
+        $this->indexer = $indexer;
+    }
+
+    /**
+     * Debug method to run a specific plugin to prepare a result object.
+     * The object is then stored in the indexer object to debug further.
+     *
+     * @param   mixed  $id  ID to index
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function debug($id)
+    {
+        // Run the setup method.
+        $this->setup();
+
+        // Get the item.
+        $item = $this->getItem($id);
+
+        // Index the item.
+        $this->index($item);
+    }
+}
diff --git a/administrator/components/com_finder/src/Indexer/DebugIndexer.php b/administrator/components/com_finder/src/Indexer/DebugIndexer.php
new file mode 100644
index 000000000000..03f5743bf00d
--- /dev/null
+++ b/administrator/components/com_finder/src/Indexer/DebugIndexer.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Finder\Administrator\Indexer;
+
+/**
+ * Debugging indexer class for the Finder indexer package.
+ *
+ * @since  __DEPLOY_VERSION__
+ * @internal
+ */
+class DebugIndexer extends Indexer
+{
+    /**
+     * The result object from the last call to self::index()
+     *
+     * @var Result
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    public static $item;
+
+    /**
+     * Stub for index() in indexer class
+     *
+     * @param   Result  $item    Result object to index
+     * @param   string  $format  Format to index
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function index($item, $format = 'html')
+    {
+        self::$item = $item;
+    }
+}
diff --git a/administrator/components/com_finder/src/Model/IndexerModel.php b/administrator/components/com_finder/src/Model/IndexerModel.php
index 46e8b6cd0d99..3328ce94788e 100644
--- a/administrator/components/com_finder/src/Model/IndexerModel.php
+++ b/administrator/components/com_finder/src/Model/IndexerModel.php
@@ -10,7 +10,8 @@
 
 namespace Joomla\Component\Finder\Administrator\Model;
 
-use Joomla\CMS\MVC\Model\BaseDatabaseModel;
+use Joomla\CMS\Form\Form;
+use Joomla\CMS\MVC\Model\FormModel;
 
 // phpcs:disable PSR1.Files.SideEffects
 \defined('_JEXEC') or die;
@@ -21,6 +22,29 @@
  *
  * @since  2.5
  */
-class IndexerModel extends BaseDatabaseModel
+class IndexerModel extends FormModel
 {
+    /**
+     * Method for getting a form.
+     *
+     * @param   array    $data      Data for the form.
+     * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
+     *
+     * @return  Form
+     *
+     * @since   __DEPLOY_VERSION__
+     *
+     * @throws \Exception
+     */
+    public function getForm($data = [], $loadData = true)
+    {
+        // Get the form.
+        $form = $this->loadForm('com_finder.indexer', 'indexer', ['control' => '', 'load_data' => $loadData]);
+
+        if (empty($form)) {
+            return false;
+        }
+
+        return $form;
+    }
 }
diff --git a/administrator/components/com_finder/src/Model/ItemModel.php b/administrator/components/com_finder/src/Model/ItemModel.php
new file mode 100644
index 000000000000..66360f75f61b
--- /dev/null
+++ b/administrator/components/com_finder/src/Model/ItemModel.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Finder\Administrator\Model;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\MVC\Model\BaseDatabaseModel;
+use Joomla\Database\ParameterType;
+
+/**
+ * Index Item model class for Finder.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class ItemModel extends BaseDatabaseModel
+{
+    /**
+     * Stock method to auto-populate the model state.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function populateState()
+    {
+        // Get the pk of the record from the request.
+        $pk = Factory::getApplication()->input->getInt('id');
+        $this->setState('item.link_id', $pk);
+    }
+
+    /**
+     * Get a finder link object
+     *
+     * @return  object
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function getItem()
+    {
+        $link_id = (int) $this->getState('item.link_id');
+        $db      = $this->getDatabase();
+        $query   = $db->getQuery(true)
+            ->select('*')
+            ->from($db->quoteName('#__finder_links', 'l'))
+            ->where($db->quoteName('l.link_id') . ' = :link_id')
+            ->bind(':link_id', $link_id, ParameterType::INTEGER);
+
+        $db->setQuery($query);
+
+        return $db->loadObject();
+    }
+
+    /**
+     * Get terms associated with a finder link
+     *
+     * @return  object[]
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function getTerms()
+    {
+        $link_id = (int) $this->getState('item.link_id');
+        $db      = $this->getDatabase();
+        $query   = $db->getQuery(true)
+            ->select('t.*, l.*')
+            ->from($db->quoteName('#__finder_links_terms', 'l'))
+            ->leftJoin($db->quoteName('#__finder_terms', 't') . ' ON ' . $db->quoteName('t.term_id') . ' = ' . $db->quoteName('l.term_id'))
+            ->where($db->quoteName('l.link_id') . ' = :link_id')
+            ->order('l.weight')
+            ->bind(':link_id', $link_id, ParameterType::INTEGER);
+
+        $db->setQuery($query);
+
+        return $db->loadObjectList();
+    }
+
+    /**
+     * Get taxonomies associated with a finder link
+     *
+     * @return  \stdClass[]
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function getTaxonomies()
+    {
+        $link_id = (int) $this->getState('item.link_id');
+        $db      = $this->getDatabase();
+        $query   = $db->getQuery(true)
+            ->select('t.*, m.*')
+            ->from($db->quoteName('#__finder_taxonomy_map', 'm'))
+            ->leftJoin($db->quoteName('#__finder_taxonomy', 't') . ' ON ' . $db->quoteName('t.id') . ' = ' . $db->quoteName('m.node_id'))
+            ->where($db->quoteName('m.link_id') . ' = :link_id')
+            ->order('t.title')
+            ->bind(':link_id', $link_id, ParameterType::INTEGER);
+
+        $db->setQuery($query);
+
+        return $db->loadObjectList();
+    }
+}
diff --git a/administrator/components/com_finder/src/View/Index/HtmlView.php b/administrator/components/com_finder/src/View/Index/HtmlView.php
index 5cd83861e7bf..5c611a8607ee 100644
--- a/administrator/components/com_finder/src/View/Index/HtmlView.php
+++ b/administrator/components/com_finder/src/View/Index/HtmlView.php
@@ -174,13 +174,35 @@ protected function addToolbar()
 
         ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder');
 
-        $toolbar->popupButton('archive', 'COM_FINDER_INDEX')
-            ->url('index.php?option=com_finder&view=indexer&tmpl=component')
-            ->iframeWidth(550)
-            ->iframeHeight(210)
-            ->onclose('window.parent.location.reload()')
-            ->icon('icon-archive')
-            ->title(Text::_('COM_FINDER_HEADING_INDEXER'));
+        if (JDEBUG) {
+            $dropdown = $toolbar->dropdownButton('indexing-group');
+            $dropdown->text('COM_FINDER_INDEX')
+                ->toggleSplit(false)
+                ->icon('icon-archive')
+                ->buttonClass('btn btn-action');
+
+            $childBar = $dropdown->getChildToolbar();
+
+            $childBar->popupButton('index', 'COM_FINDER_INDEX')
+                ->url('index.php?option=com_finder&view=indexer&tmpl=component')
+                ->icon('icon-archive')
+                ->iframeWidth(500)
+                ->iframeHeight(210)
+                ->onclose('window.parent.location.reload()')
+                ->title(Text::_('COM_FINDER_HEADING_INDEXER'));
+
+            $childBar->linkButton('indexdebug', 'COM_FINDER_INDEX_TOOLBAR_INDEX_DEBUGGING')
+                ->url('index.php?option=com_finder&view=indexer&layout=debug')
+                ->icon('icon-tools');
+        } else {
+            $toolbar->popupButton('index', 'COM_FINDER_INDEX')
+                ->url('index.php?option=com_finder&view=indexer&tmpl=component')
+                ->icon('icon-archive')
+                ->iframeWidth(500)
+                ->iframeHeight(210)
+                ->onclose('window.parent.location.reload()')
+                ->title(Text::_('COM_FINDER_HEADING_INDEXER'));
+        }
 
         if (!$this->isEmptyState) {
             if ($canDo->get('core.edit.state')) {
diff --git a/administrator/components/com_finder/src/View/Indexer/HtmlView.php b/administrator/components/com_finder/src/View/Indexer/HtmlView.php
index e13214d3d24c..a65d408621be 100644
--- a/administrator/components/com_finder/src/View/Indexer/HtmlView.php
+++ b/administrator/components/com_finder/src/View/Indexer/HtmlView.php
@@ -10,7 +10,14 @@
 
 namespace Joomla\Component\Finder\Administrator\View\Indexer;
 
+use Joomla\CMS\Factory;
+use Joomla\CMS\Form\Form;
+use Joomla\CMS\Helper\ContentHelper;
+use Joomla\CMS\Language\Text;
 use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Router\Route;
+use Joomla\CMS\Toolbar\Toolbar;
+use Joomla\CMS\Toolbar\ToolbarHelper;
 
 // phpcs:disable PSR1.Files.SideEffects
 \defined('_JEXEC') or die;
@@ -23,4 +30,55 @@
  */
 class HtmlView extends BaseHtmlView
 {
+    /**
+     * @var   Form  $form
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    public $form;
+
+    /**
+     * Method to display the view.
+     *
+     * @param   string  $tpl  A template file to load. [optional]
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function display($tpl = null)
+    {
+        if ($this->getLayout() == 'debug') {
+            $this->form = $this->get('Form');
+            $this->addToolbar();
+        }
+
+        parent::display($tpl);
+    }
+
+    /**
+     * Method to configure the toolbar for this view.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function addToolbar()
+    {
+        $toolbar = Toolbar::getInstance('toolbar');
+
+        ToolbarHelper::title(Text::_('COM_FINDER_INDEXER_TOOLBAR_TITLE'), 'search-plus finder');
+
+        $arrow  = Factory::getLanguage()->isRtl() ? 'arrow-right' : 'arrow-left';
+
+        ToolbarHelper::link(
+            Route::_('index.php?option=com_finder&view=index'),
+            'JTOOLBAR_BACK',
+            $arrow
+        );
+
+        $toolbar->standardButton('index', 'COM_FINDER_INDEX')
+            ->icon('icon-play')
+            ->onclick('Joomla.debugIndexing();');
+    }
 }
diff --git a/administrator/components/com_finder/src/View/Item/HtmlView.php b/administrator/components/com_finder/src/View/Item/HtmlView.php
new file mode 100644
index 000000000000..b3c8624b400f
--- /dev/null
+++ b/administrator/components/com_finder/src/View/Item/HtmlView.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+namespace Joomla\Component\Finder\Administrator\View\Item;
+
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
+use Joomla\CMS\Toolbar\ToolbarHelper;
+
+/**
+ * Index view class for Finder.
+ *
+ * @since  __DEPLOY_VERSION__
+ */
+class HtmlView extends BaseHtmlView
+{
+    /**
+     * The indexed item
+     *
+     * @var  object
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $item;
+
+    /**
+     * The associated terms
+     *
+     * @var  object[]
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $terms;
+
+    /**
+     * The associated taxonomies
+     *
+     * @var  object[]
+     *
+     * @since  __DEPLOY_VERSION__
+     */
+    protected $taxonomies;
+
+    /**
+     * Method to display the view.
+     *
+     * @param   string  $tpl  A template file to load. [optional]
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    public function display($tpl = null)
+    {
+        $this->item       = $this->get('Item');
+        $this->terms      = $this->get('Terms');
+        $this->taxonomies = $this->get('Taxonomies');
+
+        // Configure the toolbar.
+        $this->addToolbar();
+
+        parent::display($tpl);
+    }
+
+    /**
+     * Method to configure the toolbar for this view.
+     *
+     * @return  void
+     *
+     * @since   __DEPLOY_VERSION__
+     */
+    protected function addToolbar()
+    {
+        ToolbarHelper::title(Text::_('COM_FINDER_INDEX_TOOLBAR_TITLE'), 'search-plus finder');
+        ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_finder&view=index');
+    }
+}
diff --git a/administrator/components/com_finder/tmpl/index/default.php b/administrator/components/com_finder/tmpl/index/default.php
index c51294871e46..b30d081cf782 100644
--- a/administrator/components/com_finder/tmpl/index/default.php
+++ b/administrator/components/com_finder/tmpl/index/default.php
@@ -26,7 +26,6 @@
 $wa = $this->document->getWebAssetManager();
 $wa->useScript('multiselect')
     ->useScript('table.columns');
-
 ?>
 <form action="<?php echo Route::_('index.php?option=com_finder&view=index'); ?>" method="post" name="adminForm" id="adminForm">
     <div class="row">
@@ -111,7 +110,13 @@
                                     <?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'index.', $canChange, 'cb'); ?>
                                 </td>
                                 <th scope="row">
-                                    <?php echo $this->escape($item->title); ?>
+                                    <?php if (JDEBUG) : ?>
+                                        <a href="index.php?option=com_finder&view=item&id=<?php echo $item->link_id; ?>">
+                                            <?php echo $this->escape($item->title); ?>
+                                        </a>
+                                    <?php else : ?>
+                                        <?php echo $this->escape($item->title); ?>
+                                    <?php endif; ?>
                                 </th>
                                 <td class="small d-none d-md-table-cell">
                                     <?php
diff --git a/administrator/components/com_finder/tmpl/indexer/debug.php b/administrator/components/com_finder/tmpl/indexer/debug.php
new file mode 100644
index 000000000000..b4247b72025e
--- /dev/null
+++ b/administrator/components/com_finder/tmpl/indexer/debug.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\Language\Text;
+use Joomla\CMS\Router\Route;
+
+/** @var Joomla\Component\Finder\Administrator\View\Indexer\HtmlView $this */
+
+Text::script('COM_FINDER_INDEXER_MESSAGE_COMPLETE', true);
+
+/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */
+$wa = $this->document->getWebAssetManager();
+$wa->useScript('keepalive')
+    ->useScript('com_finder.debug');
+
+?>
+
+<form action="<?php echo Route::_('index.php?option=com_finder&layout=debug'); ?>" method="post" name="adminForm" id="debug-form">
+    <div class="form-horizontal">
+        <div class="card mt-3">
+            <div class="card-body">
+                <fieldset class="adminform p-4">
+                    <div class="alert alert-info">
+                        <h2 class="alert-heading"><?php echo Text::_('COM_FINDER_INDEXER_MSG_DEBUGGING_INDEXING'); ?></h2>
+                        <?php echo Text::_('COM_FINDER_INDEXER_MSG_DEBUGGING_INDEXING_TEXT'); ?>
+                    </div>
+                    <?php echo $this->form->renderField('plugin'); ?>
+                    <?php echo $this->form->renderField('id'); ?>
+
+                    <input id="finder-indexer-token" type="hidden" name="<?php echo Factory::getSession()->getFormToken(); ?>" value="1">
+                </fieldset>
+            </div>
+        </div>
+    </div>
+</form>
+
+<div class="form-horizontal">
+    <div class="card mt-3">
+        <div class="card-body">
+            <fieldset class="adminform">
+                <legend><?php echo Text::_('COM_FINDER_INDEXER_OUTPUT_AREA_TITLE'); ?></legend>
+                <div id="indexer-output" class="border p-3" style="min-height:200px;">
+
+                </div>
+            </fieldset>
+        </div>
+    </div>
+</div>
+
+
+
diff --git a/administrator/components/com_finder/tmpl/item/default.php b/administrator/components/com_finder/tmpl/item/default.php
new file mode 100644
index 000000000000..c76160c693c0
--- /dev/null
+++ b/administrator/components/com_finder/tmpl/item/default.php
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * @package     Joomla.Administrator
+ * @subpackage  com_finder
+ *
+ * @copyright   (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license     GNU General Public License version 2 or later; see LICENSE.txt
+ */
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Language\Text;
+?>
+<div role="main">
+    <h1 class="mb-3"><?php echo $this->item->title; ?></h1>
+    <div class="card mb-3">
+        <div class="card-header"><h2><?php echo Text::_('COM_FINDER_ITEM_FIELDSET_ITEM_TITLE'); ?></h2></div>
+        <div class="card-body">
+            <dl class="row">
+                <?php foreach ($this->item as $key => $value) : ?>
+                <dt class="col-sm-3"><?php echo $key; ?></dt>
+                <dd class="col-sm-9<?php echo $key == 'object' ? ' text-break' : '';?>"><?php echo $value; ?></dd>
+                <?php endforeach; ?>
+            </dl>
+        </div>
+    </div>
+    <div class="card mb-3">
+        <div class="card-header"><h2><?php echo Text::_('COM_FINDER_ITEM_FIELDSET_TERMS_TITLE'); ?></h2></div>
+        <div class="card-body">
+            <table class="table">
+                <caption class="visually-hidden">
+                    <?php echo Text::_('COM_FINDER_ITEM_TERMS_TABLE_CAPTION'); ?>,
+                </caption>
+                <thead>
+                <tr>
+                    <th scope="col">id</th>
+                    <th scope="col">term</th>
+                    <th scope="col">stem</th>
+                    <th scope="col">common</th>
+                    <th scope="col">phrase</th>
+                    <th scope="col">weight</th>
+                    <th scope="col">links</th>
+                    <th scope="col">language</th>
+                </tr>
+                </thead>
+                <tbody>
+                <?php foreach ($this->terms as $term) : ?>
+                    <tr>
+                        <th scope="row"><?php echo $term->term_id; ?></th>
+                        <td><?php echo $term->term; ?></td>
+                        <td><?php echo $term->stem; ?></td>
+                        <td><?php echo $term->common; ?></td>
+                        <td><?php echo $term->phrase; ?></td>
+                        <td><?php echo $term->weight; ?></td>
+                        <td><?php echo $term->links; ?></td>
+                        <td><?php echo $term->language; ?></td>
+                    </tr>
+                <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+    </div>
+    <div class="card mb-3">
+        <div class="card-header"><h2><?php echo Text::_('COM_FINDER_ITEM_FIELDSET_TAXONOMIES_TITLE'); ?></h2></div>
+        <div class="card-body">
+            <table class="table">
+                <caption class="visually-hidden">
+                    <?php echo Text::_('COM_FINDER_ITEM_TAXONOMIES_TABLE_CAPTION'); ?>,
+                </caption>
+                <thead>
+                    <tr>
+                        <th scope="col">id</th>
+                        <th scope="col">title</th>
+                        <th scope="col">alias</th>
+                        <th scope="col">lft</th>
+                        <th scope="col">path</th>
+                        <th scope="col">state</th>
+                        <th scope="col">access</th>
+                        <th scope="col">language</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach ($this->taxonomies as $taxonomy) : ?>
+                        <tr>
+                            <th scope="row"><?php echo $taxonomy->id; ?></th>
+                            <td><?php echo $taxonomy->title; ?></td>
+                            <td><?php echo $taxonomy->alias; ?></td>
+                            <td><?php echo $taxonomy->lft; ?></td>
+                            <td><?php echo $taxonomy->path; ?></td>
+                            <td><?php echo $taxonomy->state; ?></td>
+                            <td><?php echo $taxonomy->access; ?></td>
+                            <td><?php echo $taxonomy->language; ?></td>
+                        </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
diff --git a/administrator/language/en-GB/com_finder.ini b/administrator/language/en-GB/com_finder.ini
index 5630de653ae4..1bf0cf4df979 100644
--- a/administrator/language/en-GB/com_finder.ini
+++ b/administrator/language/en-GB/com_finder.ini
@@ -72,6 +72,7 @@ COM_FINDER_EMPTYSTATE_CONTENT="No content has been indexed or you have deleted a
 COM_FINDER_EMPTYSTATE_SEARCHES_CONTENT="There are no phrases used for site searching to view yet."
 COM_FINDER_FIELD_CREATED_BY_ALIAS_LABEL="Alias"
 COM_FINDER_FIELD_CREATED_BY_LABEL="Created By"
+COM_FINDER_FIELD_FINDER_PLUGIN_LABEL="Finder Plugin"
 COM_FINDER_FIELDSET_INDEX_OPTIONS_DESCRIPTION="These options influence how the content is indexed. After changing settings here, the index needs to be rebuilt."
 COM_FINDER_FIELDSET_INDEX_OPTIONS_LABEL="Index"
 COM_FINDER_FIELDSET_SEARCH_OPTIONS_LABEL="Smart Search"
@@ -152,11 +153,16 @@ COM_FINDER_INDEX_SEARCH_DESC="Search in title, URL and last updated date."
 COM_FINDER_INDEX_SEARCH_LABEL="Search Indexed Content"
 COM_FINDER_INDEX_TABLE_CAPTION="Indexed Content"
 COM_FINDER_INDEX_TIP="Start the indexer by pressing the button below, or in the toolbar."
+COM_FINDER_INDEX_TOOLBAR_INDEX_DEBUGGING="Index Debugging"
 COM_FINDER_INDEX_TOOLBAR_MAINTENANCE="Maintenance"
 COM_FINDER_INDEX_TOOLBAR_OPTIMISE="Optimise"
 COM_FINDER_INDEX_TOOLBAR_PURGE="Clear Index"
 COM_FINDER_INDEX_TOOLBAR_TITLE="Smart Search: Indexed Content"
 COM_FINDER_INDEX_TYPE_FILTER="Any Type of Content"
+COM_FINDER_INDEXER_FIELDSET_ATTRIBUTES="Result Object"
+COM_FINDER_INDEXER_FIELDSET_ELEMENTS="Additional Elements"
+COM_FINDER_INDEXER_FIELDSET_INSTRUCTIONS="Instructions"
+COM_FINDER_INDEXER_FIELDSET_TAXONOMIES="Taxonomies"
 COM_FINDER_INDEXER_HEADER_COMPLETE="Indexing Complete"
 COM_FINDER_INDEXER_HEADER_ERROR="An Error Has Occurred"
 COM_FINDER_INDEXER_HEADER_INIT="Starting Indexer"
@@ -169,6 +175,15 @@ COM_FINDER_INDEXER_MESSAGE_COMPLETE="The indexing process is complete. It is now
 COM_FINDER_INDEXER_MESSAGE_INIT="The indexer is starting. Do not close this window."
 COM_FINDER_INDEXER_MESSAGE_OPTIMIZE="The index tables are being optimised for the best possible performance. Do not close this window."
 COM_FINDER_INDEXER_MESSAGE_RUNNING="Your content is being indexed. Do not close this window."
+COM_FINDER_INDEXER_MSG_DEBUGGING_INDEXING="Debugging Smart Search indexing plugins"
+COM_FINDER_INDEXER_MSG_DEBUGGING_INDEXING_TEXT="Select a Smart Search plugin and provide an ID to index. The result of that plugin for that ID will then be displayed in the below area."
+COM_FINDER_INDEXER_OUTPUT_AREA_TITLE="Output"
+COM_FINDER_INDEXER_TOOLBAR_TITLE="Indexer: Debug Mode"
+COM_FINDER_ITEM_FIELDSET_ITEM_TITLE="Item attributes"
+COM_FINDER_ITEM_FIELDSET_TAXONOMIES_TITLE="Taxonomies"
+COM_FINDER_ITEM_FIELDSET_TERMS_TITLE="Terms"
+COM_FINDER_ITEM_TAXONOMIES_TABLE_CAPTION="Table of taxonomies"
+COM_FINDER_ITEM_TERMS_TABLE_CAPTION="Table of terms"
 COM_FINDER_ITEM_X_ONLY="%s Only"
 COM_FINDER_ITEMS="Content"
 COM_FINDER_LOGGING_DISABLED="Gathering of statistics is disabled. Enable it in the %s."
diff --git a/build/media_source/com_finder/joomla.asset.json b/build/media_source/com_finder/joomla.asset.json
index 8d562f35ec7d..6f964164c45d 100644
--- a/build/media_source/com_finder/joomla.asset.json
+++ b/build/media_source/com_finder/joomla.asset.json
@@ -10,6 +10,29 @@
       "type": "style",
       "uri": "com_finder/dates.min.css"
     },
+    {
+      "name": "com_finder.debug.es5",
+      "type": "script",
+      "uri": "com_finder/debug-es5.min.js",
+      "dependencies": [
+        "core"
+      ],
+      "attributes": {
+        "nomodule": true,
+        "defer": true
+      }
+    },
+    {
+      "name": "com_finder.debug",
+      "type": "script",
+      "uri": "com_finder/debug.min.js",
+      "dependencies": [
+        "com_finder.debug.es5"
+      ],
+      "attributes": {
+        "type": "module"
+      }
+    },
     {
       "name": "com_finder.filters.es5",
       "type": "script",
diff --git a/build/media_source/com_finder/js/debug.es6.js b/build/media_source/com_finder/js/debug.es6.js
new file mode 100644
index 000000000000..274ba59c49d3
--- /dev/null
+++ b/build/media_source/com_finder/js/debug.es6.js
@@ -0,0 +1,46 @@
+/**
+ * @copyright  (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
+ * @license    GNU General Public License version 2 or later; see LICENSE.txt
+ */
+// eslint-disable no-alert
+((Joomla, document) => {
+  'use strict';
+
+  if (!Joomla) {
+    throw new Error('core.js was not properly initialised');
+  }
+
+  Joomla.finderIndexer = () => {
+    const path = 'index.php?option=com_finder&task=indexer.debug&tmpl=component&format=json';
+    const token = `&${document.getElementById('finder-indexer-token').getAttribute('name')}=1`;
+
+    Joomla.debugIndexing = () => {
+      const formEls = new URLSearchParams(Array.from(new FormData(document.getElementById('debug-form')))).toString();
+      Joomla.request({
+        url: `${path}${token}&${formEls}`,
+        method: 'GET',
+        data: '',
+        perform: true,
+        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+        onSuccess: (response) => {
+          const output = document.getElementById('indexer-output');
+          try {
+            const parsed = JSON.parse(response);
+            output.innerHTML = parsed.rendered;
+          } catch (e) {
+            output.innerHTML = response;
+          }
+        },
+        onError: (xhr) => {
+          const output = document.getElementById('indexer-output');
+          output.innerHTML = xhr.response;
+        },
+      });
+    };
+  };
+})(Joomla, document);
+
+// @todo use directly the Joomla.finderIndexer() instead of the Indexer()!!!
+document.addEventListener('DOMContentLoaded', () => {
+  window.Indexer = Joomla.finderIndexer();
+});

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions