Enabling / Disabling Hooks

Talk about Hooks. Share Hooks you have made.
Post Reply
Omar_one
Senior Contributor
Posts: 676
Joined: Fri Oct 25, 2019 5:06 pm
Phoenix Version: v1.0.8.16
Has thanked: 100 times
Been thanked: 56 times

Enabling / Disabling Hooks

Post by Omar_one »

@azpro @beerbee @Kofod95
@ArtcoInc @14Steve14 and anyone interested in enabling/disabling Hooks.
what you think
hooks.gif
- One core-file override (hooks.php),
- sql (Adds `hooks_status`)
- 2 -3 new files
You do not have the required permissions to view the files attached to this post.


Join The Code Co-op to get access to your library in the Code Co-op Forum
User avatar
bonbec
Contributor
Posts: 190
Joined: Mon Oct 26, 2020 12:23 pm
Phoenix Version: V1.1.0.7
Has thanked: 54 times
Been thanked: 40 times

Re: No Core Changes - A different perspective

Post by bonbec »

@Omar_one
Wow! That aligns with my view on this subject—bravo!
Sincerely, bravo!
Omar_one
Senior Contributor
Posts: 676
Joined: Fri Oct 25, 2019 5:06 pm
Phoenix Version: v1.0.8.16
Has thanked: 100 times
Been thanked: 56 times

Re: No Core Changes - A different perspective

Post by Omar_one »

I plan to share the files today or tomorrow. If anyone would like to test them and help me get everything properly finalized, please let me know.
14Steve14
Senior Contributor
Posts: 921
Joined: Fri Oct 25, 2019 7:01 pm
Phoenix Version: v1.0.9.1
Has thanked: 17 times
Been thanked: 103 times

Re: No Core Changes - A different perspective

Post by 14Steve14 »

Omar_one wrote: Thu Jul 09, 2026 2:25 pm @burt @azpro @beerbee @Kofod95
@ArtcoInc @14Steve14 and anyone interested in enabling/disabling Hooks.
what you think
hooks.gif

- One core-file override (hooks.php),
- sql (Adds `hooks_status`)
- 2 -3 new files
Looks an interesting feature
azpro
Contributor
Posts: 177
Joined: Fri Nov 06, 2020 8:25 am
Phoenix Version: v1.1.0.6
Has thanked: 30 times
Been thanked: 34 times

Re: No Core Changes - A different perspective

Post by azpro »

@Omar_one

Great stuff - Exactly what I had in mind.

Thank you for sharing!
Omar_one
Senior Contributor
Posts: 676
Joined: Fri Oct 25, 2019 5:06 pm
Phoenix Version: v1.0.8.16
Has thanked: 100 times
Been thanked: 56 times

Re: No Core Changes - A different perspective

Post by Omar_one »

@azpro @beerbee @Kofod95
@ArtcoInc @14Steve14


- I added another filter for hook locations (Shop and Admin). The page will display hooks that are defined in override template also.

Code: Select all

ALTER TABLE hooks
  ADD COLUMN hooks_status ENUM('1','0') NOT NULL DEFAULT '1' AFTER hooks_method;
ALTER TABLE hooks
  ADD UNIQUE KEY hooks_unique (hooks_site, hooks_group, hooks_action, hooks_code);
Hooks_Manager.zip

⚠️ Please don't test this on the live shop, as it's still under development and testing
You do not have the required permissions to view the files attached to this post.
User avatar
burt
Core Team
Posts: 4550
Joined: Tue Oct 29, 2019 9:37 am
Phoenix Version: v1.1.0.8
Has thanked: 252 times
Been thanked: 412 times

Re: Enabling / Disabling Hooks

Post by burt »

Split from the thread No Core Changes - A different perspective
Omar_one
Senior Contributor
Posts: 676
Joined: Fri Oct 25, 2019 5:06 pm
Phoenix Version: v1.0.8.16
Has thanked: 100 times
Been thanked: 56 times

Re: Enabling / Disabling Hooks

Post by Omar_one »

@azpro @beerbee @Kofod95
@ArtcoInc @14Steve14 and anyone interested in enabling/disabling Hooks.
Disabling or enabling a hook file does not automatically toggle all of its individual methods and actions.

The ZIP folder has been updated with the fix.
Hooks_Manager_v1.1.zip
Alternatively, you can manually replace the default.php file located in:
[YOUR ADMIN]/includes/actions/modules_hooks_manager/views/

Code: Select all

<?php
/*
  $Id$
  CE Phoenix, E-Commerce made Easy
  https://phoenixcart.org
  Copyright (c) 2024 Phoenix Cart
  Released under the GNU General Public License
*/

// 1. Process Toggle Logic First
global $db, $Admin;

$site        = $_GET['site'] ?? '';
$group       = $_GET['group'] ?? '';
$code        = $_GET['code'] ?? '';
$action_name = $_GET['action_name'] ?? '';

if ($site !== '' && $group !== '' && $code !== '') {

  // Get the current status from any method of this hook
  $check = $db->query(sprintf(
    "SELECT hooks_status
       FROM hooks
      WHERE hooks_site = '%s'
        AND hooks_group = '%s'
        AND hooks_code = '%s'
      LIMIT 1",
    $db->escape($site),
    $db->escape($group),
    $db->escape($code)
  ));

  $current = '1';

  if ($hook = $check->fetch_assoc()) {
    $current = $hook['hooks_status'];
  }

  $new_status = ($current === '1') ? '0' : '1';

  // Update ALL methods belonging to this hook
  $db->query(sprintf(
    "UPDATE hooks
        SET hooks_status = '%s'
      WHERE hooks_site = '%s'
        AND hooks_group = '%s'
        AND hooks_code = '%s'",
    $db->escape($new_status),
    $db->escape($site),
    $db->escape($group),
    $db->escape($code)
  ));

  // If it's an AJAX request, respond immediately and exit
  if (($_GET['action_type'] ?? '') === 'ajax') {
    exit('success');
  }

  // For non-AJAX fallback clicks, redirect back cleanly to prevent double-submits
  $redirect_params = [];

  if (!empty($_GET['hook_location'])) {
    $redirect_params[] = 'hook_location=' . urlencode($_GET['hook_location']);
  }

  if (!empty($_GET['hook_group'])) {
    $redirect_params[] = 'hook_group=' . urlencode($_GET['hook_group']);
  }

  header('Location: ' . $Admin->link('modules_hooks_manager', implode('&', $redirect_params)));
  exit;
}

// 2. Original Page Rendering & Filter Setup
$hook_locations = [];
$hook_groups = [];

foreach ($contents as $site_key => $groups) {
  $hook_locations[$site_key] = $site_key;

  foreach ($groups as $group_key => $actions) {
    $hook_groups[$group_key] = $group_key;
  }
}

ksort($hook_locations);
ksort($hook_groups);

$selected_location = trim($_GET['hook_location'] ?? '');
$selected_group    = trim($_GET['hook_group'] ?? '');

// Build hook status lookup by site/group/code
$hook_statuses = [];

foreach ($statuses as $key => $status) {
  [$s, $g, $a, $c] = explode("\0", $key);
  $hook_statuses["{$s}\0{$g}\0{$c}"] = $status;
}

// 3. Collect and Sort Row Data by File Name
$table_rows = [];

foreach ($contents as $site => $groups) {
  if ($selected_location !== '' && strtolower(trim($site)) !== strtolower(trim($selected_location))) {
    continue;
  }

  foreach ($groups as $group => $actions) {
    if ($selected_group !== '' && strtolower(trim($group)) !== strtolower(trim($selected_group))) {
      continue;
    }

    foreach ($actions as $action => $codes) {
      foreach ($codes as $code => $locations) {
        foreach ($locations as $location) {

          if (is_array($location)) {
            $file = implode('->', $location);
            $class = !empty($location[0]) ? explode('::', $location[0])[0] : null;
            $version = ($class && class_exists($class))
              ? (get_class_vars($class)['version'] ?? null)
              : null;
          } else {
            $file = "$code.php";
            $version = get_class_vars("hook_{$site}_{$group}_{$code}")['version'] ?? null;
          }

          // Use one status for the entire hook file
          $hook_key = "{$site}\0{$group}\0{$code}";
          $enabled = ($hook_statuses[$hook_key] ?? '1') === '1';

          $current_script = basename($_SERVER['PHP_SELF']);

          $query_string = 'app=hooks'
            . '&site=' . urlencode($site)
            . '&group=' . urlencode($group)
            . '&action_name=' . urlencode($action)
            . '&code=' . urlencode($code);

          if (!empty($selected_location)) {
            $query_string .= '&hook_location=' . urlencode($selected_location);
          }

          if (!empty($selected_group)) {
            $query_string .= '&hook_group=' . urlencode($selected_group);
          }

          $toggle_href = $current_script . '?' . $query_string;

          $table_rows[] = [
            'site'        => $site,
            'group'       => $group,
            'file'        => $file,
            'action'      => $action,
            'version'     => $version,
            'enabled'     => $enabled,
            'toggle_href' => $toggle_href
          ];
        }
      }
    }
  }
}

// Perform alphabetical sort on the computed filename string
usort($table_rows, function ($a, $b) {
  return strcasecmp($a['file'], $b['file']);
});
?>

<form method="get" class="mb-3">
<?php
foreach ($_GET as $k => $v) {
  if (!in_array($k, ['hook_location', 'hook_group', 'site', 'group', 'action_name', 'code', 'action_type'])) {
?>
<input type="hidden" name="<?= htmlspecialchars($k) ?>" value="<?= htmlspecialchars($v) ?>">
<?php
  }
}
?>

<div class="row mb-3">
  <div class="col-md-4 mb-2 mb-md-0">
    <select name="hook_location" class="form-select" onchange="this.form.submit()">
      <option value="">All Locations</option>
      <?php foreach ($hook_locations as $location) { ?>
        <option value="<?= htmlspecialchars($location) ?>" <?= ($selected_location === $location ? 'selected' : '') ?>>
          <?= htmlspecialchars($location) ?>
        </option>
      <?php } ?>
    </select>
  </div>

  <div class="col-md-4">
    <select name="hook_group" class="form-select" onchange="this.form.submit()">
      <option value="">All Hook Groups</option>
      <?php foreach ($hook_groups as $group_item) { ?>
        <option value="<?= htmlspecialchars($group_item) ?>" <?= ($selected_group === $group_item ? 'selected' : '') ?>>
          <?= htmlspecialchars($group_item) ?>
        </option>
      <?php } ?>
    </select>
  </div>
</div>
</form>

<div class="table-responsive">
  <table class="table table-striped table-hover">
    <thead class="table-dark">
      <tr>
        <th colspan="5"><?= defined('TABLE_HEADING_HOOKS') ? TABLE_HEADING_HOOKS : 'Registered Hooks' ?></th>
      </tr>
    </thead>
    <thead class="table-light">
      <tr>
        <th><?= TABLE_HEADING_GROUP ?></th>
        <th><?= TABLE_HEADING_FILE ?></th>
        <th><?= TABLE_HEADING_METHOD ?></th>
        <th class="text-end"><?= TABLE_HEADING_VERSION ?></th>
        <th class="text-end"><?= TABLE_HEADING_STATUS ?></th>
      </tr>
    </thead>
    <tbody>
      <?php foreach ($table_rows as $row) { ?>
       <tr
  class="<?= $row['enabled'] ? '' : 'text-muted' ?>"
  data-hook-site="<?= htmlspecialchars($row['site']) ?>"
  data-hook-group="<?= htmlspecialchars($row['group']) ?>"
  data-hook-file="<?= htmlspecialchars($row['file']) ?>"
>
          <td><?= htmlspecialchars($row['group']) ?></td>
          <td><?= htmlspecialchars($row['file']) ?></td>
          <td><?= htmlspecialchars($row['action']) ?></td>
          <td class="text-end"><?= htmlspecialchars($row['version'] ?? 'N/A') ?></td>
          <td class="text-end">
            <a href="<?= $row['toggle_href'] ?>"
               class="js-hook-toggle"
               title="<?= $row['enabled'] ? TEXT_CLICK_TO_DISABLE : TEXT_CLICK_TO_ENABLE ?>">
              <i class="fa fa-toggle-<?= $row['enabled'] ? 'on text-success' : 'off text-muted' ?> fa-lg"></i>
            </a>
          </td>
        </tr>
      <?php } ?>
    </tbody>
  </table>
</div>

<hr>

<p><?= TEXT_HOOKS_DIRECTORY . ' ' . DIR_FS_CATALOG . 'includes/hooks/' ?></p>

<script>
document.addEventListener('DOMContentLoaded', function () {

  document.querySelectorAll('.js-hook-toggle').forEach(function (link) {

    link.addEventListener('click', function (e) {
      e.preventDefault();

      const clickedRow = this.closest('tr');
      const site = clickedRow.dataset.hookSite;
      const group = clickedRow.dataset.hookGroup;
      const file = clickedRow.dataset.hookFile;

      // All rows belonging to the same hook file
      const rows = document.querySelectorAll(
        `tr[data-hook-site="${site}"][data-hook-group="${group}"][data-hook-file="${file}"]`
      );

      const turningOff =
        this.querySelector('i').classList.contains('fa-toggle-on');

      // Update all matching rows immediately
      rows.forEach(function (row) {
        const icon = row.querySelector('.js-hook-toggle i');
        const link = row.querySelector('.js-hook-toggle');

        if (turningOff) {
          icon.classList.remove('fa-toggle-on', 'text-success');
          icon.classList.add('fa-toggle-off', 'text-muted');
          row.classList.add('text-muted');
          link.title = '<?= addslashes(TEXT_CLICK_TO_ENABLE) ?>';
        } else {
          icon.classList.remove('fa-toggle-off', 'text-muted');
          icon.classList.add('fa-toggle-on', 'text-success');
          row.classList.remove('text-muted');
          link.title = '<?= addslashes(TEXT_CLICK_TO_DISABLE) ?>';
        }
      });

      const ajaxUrl = this.href + '&action_type=ajax';

      fetch(ajaxUrl)
        .then(function (response) {
          if (!response.ok) {
            throw new Error('AJAX request failed');
          }
        })
        .catch(function (error) {
          console.error(error);

          // Roll back all rows if the request fails
          rows.forEach(function (row) {
            const icon = row.querySelector('.js-hook-toggle i');
            const link = row.querySelector('.js-hook-toggle');

            if (turningOff) {
              icon.classList.remove('fa-toggle-off', 'text-muted');
              icon.classList.add('fa-toggle-on', 'text-success');
              row.classList.remove('text-muted');
              link.title = '<?= addslashes(TEXT_CLICK_TO_DISABLE) ?>';
            } else {
              icon.classList.remove('fa-toggle-on', 'text-success');
              icon.classList.add('fa-toggle-off', 'text-muted');
              row.classList.add('text-muted');
              link.title = '<?= addslashes(TEXT_CLICK_TO_ENABLE) ?>';
            }
          });
        });

    });

  });

});
</script>
You do not have the required permissions to view the files attached to this post.


Join The Code Co-op to get access to your library in the Code Co-op Forum
Post Reply