# extendify/3.1.5/app/Agent/Controllers/ContentController.php

Extendify, version 3.1.5. 360 lines.

- Page: https://pluginprobe.com/plugins/extendify/3.1.5/code/app/Agent/Controllers/ContentController.php
- Raw: https://pluginprobe.com/plugins/extendify/3.1.5/raw/app/Agent/Controllers/ContentController.php
- Modified: 2026-08-20T19:51:40+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/extendify/3.1.5/code/app/Agent/Controllers/ContentController.php#L10-L20`.

```php
<?php

namespace Extendify\Agent\Controllers;

defined('ABSPATH') || die('No direct access.');

class ContentController
{
    // phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
    // Header and footer text lives in a template part, a synced pattern's in a wp_block.
    const POST_TYPES = ['post', 'page', 'wp_template_part', 'wp_block'];

    const FORMATS = ['text', 'blocks', 'excerpt'];

    const MAX_SCANNED = 100;

    const MAX_POSTS = 25;

    const MAX_MATCHES = 3;

    const MAX_CONTEXT = 300;

    const MAX_CONTENT = 15000;

    const MAX_BYTES = 20000;

    const MAX_META_KEYS = 50;

    const MAX_META_VALUE = 500;
    // phpcs:enable PSR12.Properties.ConstantVisibility.NotFound

    /**
     * @param \WP_REST_Request $request The REST API request object.
     * @return \WP_REST_Response
     */
    public static function search(\WP_REST_Request $request)
    {
        $params = self::params($request);
        $query = trim(self::text($params, 'query'));
        if ($query === '') {
            return self::reject('query must say what to look for');
        }

        $types = self::types($params);
        if (!$types) {
            return self::reject('postTypes names no post type this site has');
        }

        $limit = self::number($params, 'limit', 10, self::MAX_POSTS);
        $context = self::number($params, 'context', 60, self::MAX_CONTEXT);
        $candidates = new \WP_Query([
            'post_type' => $types,
            'post_status' => 'publish',
            'posts_per_page' => self::MAX_SCANNED,
            'orderby' => 'modified',
            'order' => 'DESC',
            'ignore_sticky_posts' => true,
        ]);

        $results = [];
        $matched = 0;
        $spent = 0;
        foreach ($candidates->posts as $post) {
            $found = self::snippets(self::rendered($post), $query, $context);
            if (!$found['matches']) {
                continue;
            }

            $matched++;
            if (count($results) >= $limit || $spent > self::MAX_BYTES) {
                continue;
            }

            $result = [
                'id' => (int) $post->ID,
                'type' => $post->post_type,
                'title' => self::decoded($post->post_title),
                'slug' => $post->post_name,
                'matches' => $found['matches'],
                'moreMatches' => $found['count'] - count($found['matches']),
            ];
            // Escaped JSON counts one Japanese character as six, spending the budget six times over.
            $spent += strlen((string) wp_json_encode($result, JSON_UNESCAPED_UNICODE));
            $results[] = $result;
        }

        $response = [
            'query' => $query,
            'scanned' => count($candidates->posts),
            'matched' => $matched,
            'results' => $results,
        ];
        $left = self::leftOut($matched - count($results), (int) $candidates->found_posts - count($candidates->posts));
        if ($left !== '') {
            $response['truncated'] = $left;
        }

        return new \WP_REST_Response($response);
    }

    /**
     * @param \WP_REST_Request $request The REST API request object.
     * @return \WP_REST_Response
     */
    public static function read(\WP_REST_Request $request)
    {
        $params = self::params($request);
        $id = self::number($params, 'id', 0, PHP_INT_MAX);
        $slug = trim(self::text($params, 'slug'));
        if (!$id && $slug === '') {
            return self::reject('id or slug must say what to read');
        }

        $format = self::text($params, 'format', 'text');
        if (!in_array($format, self::FORMATS, true)) {
            return self::reject('format must be one of ' . implode(', ', self::FORMATS));
        }

        $post = $id ? self::byId($id) : self::bySlug($slug);
        if (!$post) {
            return self::reject('Nothing readable was found for ' . ($id ? $id : $slug));
        }

        $content = self::content($post, $format);
        $response = [
            'id' => (int) $post->ID,
            'type' => $post->post_type,
            'title' => self::decoded($post->post_title),
            'slug' => $post->post_name,
            'status' => $post->post_status,
            'date' => $post->post_date,
            'format' => $format,
            'content' => mb_substr($content, 0, self::MAX_CONTENT),
        ];
        if (mb_strlen($content) > self::MAX_CONTENT) {
            $response['truncated'] = 'Cut after ' . self::MAX_CONTENT . ' characters, of '
                . mb_strlen($content) . ' the post holds.';
        }
        if (!empty($params['includeMeta'])) {
            $response['meta'] = self::meta($post->ID);
        }

        return new \WP_REST_Response($response);
    }

    /**
     * @param \WP_REST_Request $request The REST API request object.
     * @return array The request body.
     */
    private static function params(\WP_REST_Request $request)
    {
        $params = $request->get_json_params();

        return is_array($params) ? $params : [];
    }

    /**
     * @param string $message What the caller has to change.
     * @return \WP_REST_Response
     */
    private static function reject($message)
    {
        return new \WP_REST_Response(['error' => $message], 400);
    }

    /**
     * @param array  $params  The array to read from.
     * @param string $key     The key to read.
     * @param string $default What to use when the key is absent.
     * @return string
     */
    private static function text($params, $key, $default = '')
    {
        return isset($params[$key]) && is_string($params[$key]) ? $params[$key] : $default;
    }

    /**
     * @param array   $params  The array to read from.
     * @param string  $key     The key to read.
     * @param integer $default What to use when the key is absent.
     * @param integer $most    The largest value to allow.
     * @return integer
     */
    private static function number($params, $key, $default, $most)
    {
        $value = isset($params[$key]) && is_numeric($params[$key])
            ? (int) $params[$key]
            : $default;

        return max(0, min($value, $most));
    }

    /**
     * @param array $params The request body.
     * @return array The post types to search through.
     */
    private static function types($params)
    {
        $requested = isset($params['postTypes']) && is_array($params['postTypes'])
            ? $params['postTypes']
            : [];
        if (!$requested) {
            return self::POST_TYPES;
        }

        return array_values(array_filter($requested, function ($type) {
            return is_string($type) && post_type_exists($type);
        }));
    }

    /**
     * An empty result and one nobody looked for read the same to a model.
     *
     * @param integer $matched   Matching posts the response left out.
     * @param integer $unscanned Candidates the scan never reached.
     * @return string
     */
    private static function leftOut($matched, $unscanned)
    {
        $left = [];
        if ($matched > 0) {
            $left[] = $matched . ' more posts matched';
        }
        if ($unscanned > 0) {
            $left[] = $unscanned . ' posts were not searched';
        }

        return $left ? implode(', and ', $left) . '.' : '';
    }

    /**
     * Blocks that read the post they sit in render empty without the global.
     *
     * @param \WP_Post $post The post to render.
     * @return string Its text as a visitor reads it.
     */
    private static function rendered($post)
    {
        $GLOBALS['post'] = $post;
        setup_postdata($post);
        $html = do_shortcode(do_blocks($post->post_content));
        wp_reset_postdata();

        return trim(preg_replace('/\s+/u', ' ', self::decoded(wp_strip_all_tags($html))));
    }

    /**
     * @param string $text Text as WordPress stored or rendered it.
     * @return string
     */
    private static function decoded($text)
    {
        return html_entity_decode($text, ENT_QUOTES, get_bloginfo('charset'));
    }

    /**
     * @param string  $text    The text to look through.
     * @param string  $query   What to look for.
     * @param integer $context How much to quote either side of a match.
     * @return array The snippets, and how many times the text says it.
     */
    private static function snippets($text, $query, $context)
    {
        $length = mb_strlen($query);
        $end = mb_strlen($text);

        $matches = [];
        $count = 0;
        $quoted = 0;
        $offset = 0;
        // Turkish İ lower cases to two characters, so a folded copy's offsets miss the match.
        while (($at = mb_stripos($text, $query, $offset)) !== false) {
            $count++;
            $offset = $at + $length;
            if (count($matches) >= self::MAX_MATCHES || $at < $quoted) {
                continue;
            }

            $from = max(0, $at - $context);
            $quoted = min($end, $at + $length + $context);
            $matches[] = ($from > 0 ? '…' : '')
                . mb_substr($text, $from, $quoted - $from)
                . ($quoted < $end ? '…' : '');
        }

        return ['matches' => $matches, 'count' => $count];
    }

    /**
     * @param integer $id A post id.
     * @return \WP_Post|null
     */
    private static function byId($id)
    {
        $post = get_post($id);
        if (!$post || in_array($post->post_status, ['auto-draft', 'trash'], true)) {
            return null;
        }

        return $post;
    }

    /**
     * @param string $slug A post slug.
     * @return \WP_Post|null
     */
    private static function bySlug($slug)
    {
        $posts = get_posts([
            'name' => $slug,
            'post_type' => self::POST_TYPES,
            'post_status' => 'any',
            'posts_per_page' => 1,
        ]);

        return $posts ? $posts[0] : null;
    }

    /**
     * @param \WP_Post $post   The post being read.
     * @param string   $format Which of self::FORMATS to return.
     * @return string
     */
    private static function content($post, $format)
    {
        if ($format === 'blocks') {
            return $post->post_content;
        }

        if ($format === 'excerpt') {
            return get_the_excerpt($post);
        }

        return self::rendered($post);
    }

    /**
     * Keys under an underscore are WordPress and plugin bookkeeping.
     *
     * @param integer $id The post to read meta from.
     * @return array Name => value pairs.
     */
    private static function meta($id)
    {
        $meta = [];
        foreach (get_post_meta($id) as $key => $stored) {
            if (strpos($key, '_') === 0 || count($meta) >= self::MAX_META_KEYS) {
                continue;
            }

            $value = maybe_unserialize(isset($stored[0]) ? $stored[0] : '');
            $meta[$key] = is_string($value) && mb_strlen($value) > self::MAX_META_VALUE
                ? mb_substr($value, 0, self::MAX_META_VALUE)
                : $value;
        }

        return $meta;
    }
}

```
