XF 2.2 Getting Navigation Links via API

FSPHosting

New member
Hello,

I'm attempting to get Navigation Links via the API, to use for integration to a third party website. However, the content that is returned is challenging to work with. Im currently running the following code in my API:

PHP:
/** @var \XF\Repository\Navigation $navRepo */
$navRepo = $this->app->repository( 'XF:Navigation' );
$navTree = $navRepo->createNavigationTree();
$data = $navTree->getDescendants();

foreach ($data as $key => $navItem) {
    print_r("Key: " . $key . "\r\n");
    print_r("Title: " . $navItem->getTitle() . "\r\n");
    print_r("Link: " . $navItem->getValue("type_config")['link'] . "\r\n");
    print_r("Parent ID: " . $navItem->getValue('parent_navigation_id') . "\r\n");
}

This is generating me the following (3 different keys):

Key: home
Title: Home
Link: {$xf.homePageUrl}
Parent ID:

Key: forums
Title: Forums
Link: {{ link('forums') }}
Parent ID:

Key: newPosts
Title: New posts
Link: {{ link('whats-new/posts') }}
Parent ID: forums

I'm struggling to work out how to turn "{$xf.homePageUrl}" and "{{ link('forums') }}" into actual links. My external site is not accessing XenForo directly (via files) so cannot use any templating.

Any help would be much appreciated.
 
I've solved this in a roundabout away... it's not ideal, but it seems to work for my use case.. Would be great to see if anyone can provide a more resilient solution.

My solution was to totally bypass the templating in XenForo and instead parse the links manually in PHP in the consuming application. This meant replacing variables, such as $xf.homePageUrl and $xf.time, but also resolve link() functions, with all 3 parameters.

My API endpoint code
PHP:
public function actionGet()
{
    /** @var \XF\Repository\Navigation $navRepo */
    $navRepo      = $this->app->repository( 'XF:Navigation' );
    $navCacheData = $navRepo->createNavigationTree();
    $data = $navCacheData->getDescendants();

    $navItems = [];

    foreach ($data as $key => $navItem) {
        if ($key == "_default") continue;
        if ($navItem->getValue('parent_navigation_id') == "_default") continue;

        $navItems[$key] = [
            "title" => $navItem->getTitle(),
            "parent_id" => $navItem->getValue('parent_navigation_id'),
            "link" => trim(str_replace("\n", "", $navItem->getValue("type_config")['link']))
        ];
    }

    return $this->apiSuccess([
        'data' => $navItems
    ]);
}

My Processor Code (Consumer of endpoint, in Laravel)
PHP:
class XenForoNavigation {
    private XenForoApi $api;

    public function __construct(XenForoApi $api) {
        $this->api = $api;
    }

    public function getNavigation(): array {
        $navData = $this->api->getNavigation();
        if (!$navData['success']) {
            return;
        }

        $data = [];

        // We now have the nav data, lets extract the links
        foreach ($navData['data'] as $key => $navItem) {
            $link = $this->replaceVariables($navItem['link']);
            $linkParts = $this->extractLink($link);
            if ($linkParts !== null) {
                $link = $this->processLinkParts($linkParts);
            } else {
                $link = $this->isJsonString($link) ? $this->trimBrackets($link) : $link;
            }

            $data[$key] = [
                'title' => $navItem['title'],
                'link' => $link,
                'parent' => $navItem['parent_id'],
            ];
        }

        return $data;
    }

    private function processLinkParts(array $linkParts): string {
        $link = '';
        foreach ($linkParts as $key => $linkPart) {
            if ($key == 0) {
                $link = $linkPart;
            } else if ($key == 1) {
                if ($linkPart == "null") { continue; }
                if ($linkPart == "-") {
                    $link = implode("/-/", explode('/', $link));
                }
            } else if ($key == 2) {
                $jsonParams = str_replace("'", '"', $linkPart);
                if (!$this->isValidJson($jsonParams)) { continue; }
                $params = json_decode($jsonParams);
                $link .= "/";
                foreach($params as $paramKey => $paramValue) {
                    $link .= "&$paramKey=$paramValue";
                }
            }
        }

        return config('xenforo.url') . "/index.php?" . $link;
    }

    private function replaceVariables(string $value): string {
        // set up array of items to replace
        $toReplace = [
            "\$xf.homePageUrl" => config('app.url'),
            "\$xf.time" => time(),
        ];

        // replace items
        foreach ($toReplace as $key => $replace) {
            $value = str_replace($key, $replace, $value);
        }

        // return finished string
        return $value;
    }

    private function isJsonString(string $str): bool {
        return str_starts_with($str, "{") && str_ends_with($str, "}");
    }

    private function trimBrackets(string $str): string {
        if(strlen($str) <= 2) { return ""; }
        return substr($str, 1, -1);
    }

    private function isValidJson(string $str): bool {
        json_decode($str);
        return json_last_error() == JSON_ERROR_NONE;
    }

    private function extractLink(string $str): ?array {
        $matches = array();
        if (preg_match_all("/\(([^()]+)\)/", $str, $matches)) {
            $result = array_map("trim", explode(",", $matches[1][0]));
            return array_map(function ($item) {
                if ($item[0] === "'" && $item[strlen($item) - 1] === "'") {
                    return substr($item, 1, -1);
                }
                return $item;
            }, $result);
        } else {
            return null;
        }
    }
}

It successfully turns:
JSON:
"home": {
    "title": "Home",
    "parent_id": "",
    "link": "{$xf.homePageUrl}"
},
"home_features": {
    "title": "Features",
    "parent_id": "home",
    "link": "{$xf.homePageUrl}/features"
},
"forums": {
    "title": "Forums",
    "parent_id": "",
    "link": "{{ link('forums') }}"
}
"markForumsRead": {
    "title": "Mark forums read",
    "parent_id": "forums",
    "link": "{{ link('forums/mark-read', '-', {'date': $xf.time}) }}"
},

Into:
JSON:
"home":{
    "title":"Home",
    "link":"http://domain.test",
    "parent":""
},
"home_features":{
    "title":"Features",
    "link":"http://domain.test/features",
    "parent":"home"
},
"forums":{
    "title":"Forums",
    "link":"http://forum.domain.test/index.php?forums",
    "parent":""
},
"markForumsRead":{
    "title":"Mark forums read",
    "link":"http://forum.domain.test/index.php?forums/-/mark-read/&date=1684716528",
    "parent":"forums"
}
 
Last edited:
Top Bottom