Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.0] Smart Search: Add debugging features #2827

Closed
jgerman-bot opened this issue May 30, 2023 · 0 comments · Fixed by #2941
Closed

[5.0] Smart Search: Add debugging features #2827

jgerman-bot opened this issue May 30, 2023 · 0 comments · Fixed by #2941

Comments

@jgerman-bot
Copy link

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();
+});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging a pull request may close this issue.

4 participants