# Hooks - Pruneline 1.x documentation

> The filters and actions for the scan, what counts as registered, templates, leftover data, the audits, the report and add-ons.

Pruneline's code lives in the `Extendiment\Pruneline` namespace. The scan writes an index, the table `{prefix}pruneline_refs`: one row per thing found in one place, with `kind` (`block`, `widget`, `module`, `template`, `pattern`, `part`, `navigation`, `hidden`), `name`, `builder` (`block-editor`, `elementor`, `beaver`, `siteorigin`, `bricks`, `oxygen`, `divi`, `wpbakery`), `object_type` (`post`, `widget`, `theme`), `object_id`, `status` and `uses`. Read it with `Index::names( $kind, $builder )`, `Index::places( $kind, $name, $builder )` and `Index::in_place( $kind, $object_type, $object_id, $builder )`.

An owner is an array with `type` (`plugin`, `theme`, `mu-plugin`, `core` or `unknown`), `id` (a plugin's basename or a theme's stylesheet) and `name`. `Owners::state( $owner )` returns `active`, `inactive` or `deleted`.

The constants `PRUNELINE_VERSION`, `PRUNELINE_FILE`, `PRUNELINE_URL` and `PRUNELINE_DIR` are defined for add-ons.

## Loading, access and settings

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_loaded` (action) | none | Pruneline has loaded (on `plugins_loaded`). Its classes and constants are available. Add-ons hook their features here. |
| `pruneline_capability` (filter) | `$capability`: `manage_options` | Who can open **Tools → Pruneline**, run the scan, download CSV and move templates and patterns to the Trash. |
| `pruneline_cleanup_capability` (filter) | `$capability`: `manage_options` | Who can export and delete leftover data. Users need this and `pruneline_capability`. |
| `pruneline_settings` (action) | `$page`: the settings page's slug, `pruneline-settings` | Add sections and fields to **Settings → Pruneline**. Register your options in the `$page` group, so the page's form saves them. |

```php
add_action( 'pruneline_settings', function ( $page ) {
	register_setting( $page, 'my_cleanup_notes', array( 'sanitize_callback' => 'sanitize_textarea_field' ) );
	add_settings_section( 'my_notes', 'Notes', '__return_false', $page );
	add_settings_field( 'my_notes_field', 'Cleanup notes', function () {
		printf( '<textarea name="my_cleanup_notes" class="large-text" rows="3">%s</textarea>', esc_textarea( get_option( 'my_cleanup_notes', '' ) ) );
	}, $page, 'my_notes' );
} );

// Let editors see the audits, but keep deleting data for administrators.
add_filter( 'pruneline_capability', function () {
	return 'edit_others_pages';
} );
```

## The scan

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_sources` (filter) | `$sources`: `Sources\Source[]`, by default `Sources\Posts`, `Sources\Widgets` and `Sources\ThemeFiles` | The places the scan reads. Add an object that implements `Extendiment\Pruneline\Sources\Source`. |
| `pruneline_scan_finished` (action) | `$state`: the scan's progress, with `status` (`done`), `scan` (its number), `done`, `total`, `started` and `finished` (timestamps) | A full scan has finished, after the rows it didn't see are removed. |

A `Source` has three methods: `key()` (the object type stored with each row, at most 20 characters), `total()` (how many objects a scan reads, for the progress bar) and `batch( $cursor, $limit )`. `batch()` returns `cursor`, `done` and `items`, each item with `id`, `status` (such as `publish`; anything but `publish` or `private` counts as a draft) and `refs`, an `Extendiment\Pruneline\Refs` object. Fill `refs` with `$refs->add( $kind, $name, $builder, $uses )`, or with the extractors: `Extract\Blocks::refs( $markup )`, `Extract\Elementor::refs( $data )`, `Extract\Shortcodes::refs( $content )`. The rows of a source that is gone are removed when the next scan finishes.

```php
// Scan block markup kept in a custom option, one place per entry.
class My_Landing_Source implements \Extendiment\Pruneline\Sources\Source {
	public function key(): string {
		return 'my_landing';
	}
	public function total(): int {
		return count( (array) get_option( 'my_landing_pages', array() ) );
	}
	public function batch( string $cursor, int $limit ): array {
		$items = array();
		foreach ( (array) get_option( 'my_landing_pages', array() ) as $id => $markup ) {
			$items[] = array(
				'id'     => (string) $id,
				'status' => 'publish',
				'refs'   => \Extendiment\Pruneline\Extract\Blocks::refs( (string) $markup ),
			);
		}
		return array( 'cursor' => '', 'done' => true, 'items' => $items );
	}
}
add_filter( 'pruneline_sources', function ( $sources ) {
	$sources[] = new My_Landing_Source();
	return $sources;
} );

// Email the site admin when a scan finishes.
add_action( 'pruneline_scan_finished', function ( $state ) {
	$missing = \Extendiment\Pruneline\Audits\Registry::get( 'blocks' )->filtered( 'missing' );
	if ( $missing ) {
		wp_mail( get_option( 'admin_email' ), 'Missing blocks', implode( ', ', wp_list_pluck( $missing, 'block' ) ) );
	}
} );
```

## What counts as registered

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_registered` (filter) | `$state`: null; `$builder`; `$kind`: `block`, `widget` (Elementor) or `module`; `$name` | Decide whether a block, Elementor widget or builder module works now. Return `registered`, `missing`, `unsure` (shown as "Needs confirmation") or `inactive`; return null to let Pruneline check. It's only asked while the builder is active. |

```php
// My plugin registers acme/map only in the front end's JavaScript.
add_filter( 'pruneline_registered', function ( $state, $builder, $kind, $name ) {
	return 'block-editor' === $builder && 'acme/map' === $name ? 'registered' : $state;
}, 10, 4 );
```

## Templates

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_template_types` (filter) | `$types`: post type => builder. By default `wp_block` => `block-editor`, `elementor_library` => `elementor`, `fl-builder-template` => `beaver`, `et_pb_layout` => `divi`, `bricks_template` => `bricks`, `ct_template` => `oxygen` | The post types listed as saved templates. `wp_block` (synced patterns) is shown on **Patterns & parts**, the others on **Templates**. Post types that don't exist are skipped. |
| `pruneline_template_applied` (filter) | `$state`: `none`; `$id`: the template's post ID; `$builder` | How a template with no reference is applied: return one of `conditions`, `triggers`, `kit` or `none`. Asked for builders other than Elementor, Beaver Builder, Bricks and Oxygen, which Pruneline checks itself. |

A template counts as used when the index has a `template` row whose name is its post ID (for Beaver Builder, also its `_fl_builder_template_id`) for the same builder. A source can add such rows with `$refs->add( 'template', (string) $id, $builder )`.

```php
// Divi layouts my theme applies through its own settings.
add_filter( 'pruneline_template_applied', function ( $state, $id, $builder ) {
	return 'divi' === $builder && in_array( $id, array_map( 'intval', (array) get_option( 'my_theme_layouts', array() ) ), true ) ? 'conditions' : $state;
}, 10, 3 );
```

## Leftover data

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_protected` (filter) | `$protected`: bool, true for the data of one half of a plugin pair (`<folder>` and `<folder>-pro`) while the other half is active; `$item`: `type` (`option`, `table`, `cron`, `action`, `postmeta`, `usermeta`), `name`, `owner`, `state`, `confidence` (`exact` or `prefix`), and more | Whether a leftover is kept on purpose. It stays listed, marked "(kept on purpose)", without a checkbox. |
| `pruneline_cleaned` (action) | `$items`: the items deleted, each with `type`, `name`, `size`, `owner`, `state`, `confidence` and `detail` | Pruneline has deleted leftover data, after the export and confirmation. |

```php
// Never offer to delete the old shop's tables: the accountant still reads them.
add_filter( 'pruneline_protected', function ( $protected, $item ) {
	return 'table' === $item['type'] && false !== strpos( $item['name'], 'oldshop_' ) ? true : $protected;
}, 10, 2 );

add_action( 'pruneline_cleaned', function ( $items ) {
	error_log( 'Pruneline deleted: ' . implode( ', ', wp_list_pluck( $items, 'name' ) ) );
} );
```

## Audits and the report

| Hook | Parameters | What it's for |
|---|---|---|
| `pruneline_audits` (filter) | `$audits`: `Audits\Audit[]`: `blocks`, `builders`, `templates`, `patterns`, `hidden`, `usage`, `plugins`, `data` | The lists on **Tools → Pruneline**. An audit you add gets a tab of its own, a line in the overview, CSV and `wp pruneline list <key>`. |
| `pruneline_report` (filter) | `$data`: `site`, `editors`, `plugins`, `templates`, `colors`, `fonts`, `addons`, `blocks`, `patterns` | The handover report's data, on screen and in `wp pruneline report`. The screen shows the sections it knows; extra keys only appear in the JSON. |

An audit extends `Extendiment\Pruneline\Audits\Audit` and implements `key()`, `title()`, `intro()` (one sentence), `columns()` (key => label) and `rows()`. Each row holds a value per column, plus `_id`. Optionally a row has `_links` (column => URL), `_view` (the views it belongs to) and `_bulk` (whether it can be selected). Override `views()`, `bulk_actions()` and `bulk( $action, $ids )` for views and a bulk action, `has_detail()`, `detail( $id )` and `detail_columns()` for a **Where used** page, and `count()` for the overview's number.

```php
class My_Drafts_Audit extends \Extendiment\Pruneline\Audits\Audit {
	public function key(): string { return 'old_drafts'; }
	public function title(): string { return 'Old drafts'; }
	public function intro(): string { return 'Drafts nobody has touched for a year.'; }
	public function columns(): array { return array( 'title' => 'Title', 'modified' => 'Last changed' ); }
	public function rows(): array {
		$rows = array();
		foreach ( get_posts( array( 'post_status' => 'draft', 'post_type' => 'any', 'numberposts' => -1, 'date_query' => array( array( 'column' => 'post_modified', 'before' => '1 year ago' ) ) ) ) as $post ) {
			$rows[] = array(
				'_id'      => (string) $post->ID,
				'_links'   => array( 'title' => get_edit_post_link( $post->ID, 'raw' ) ),
				'title'    => $post->post_title,
				'modified' => $post->post_modified,
			);
		}
		return $rows;
	}
}
add_filter( 'pruneline_audits', function ( $audits ) {
	$audits[] = new My_Drafts_Audit();
	return $audits;
} );

// Leave the active plugins out of the printed report.
add_filter( 'pruneline_report', function ( $data ) {
	$data['plugins'] = array();
	return $data;
} );
```

## REST and WP-CLI

The screen uses the REST namespace `pruneline/v1`: `GET scan` (the scan's progress) and `POST scan` (runs the scan for a few seconds, starting one with `restart=true` or when none is running; returns `status`, `done`, `total` and `percent`), and `POST editor-blocks` (the block names the editor knows), all for users with `pruneline_capability`. `GET shortcodes?post=…` (the shortcodes on an Elementor page) is for users who can edit that post.

On the command line: `wp pruneline scan`, `wp pruneline list <audit> [--view=<view>] [--item=<id>] [--format=<format>]` and `wp pruneline report`. See [WP-CLI](wp-cli).

Source: https://extendiment.com/plugins/pruneline/docs/1.x/hooks
