/ tests / ControllerTest.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\controllers\Controller;
use seekquarry\yioop\controllers\StaticController;
use seekquarry\yioop\library\UnitTest;

/**
 * Checks the parts of the controller every controller inherits: the
 * cleaning of values that came from a person, and the reduction of a folder
 * path they named to one that cannot reach outside where it is resolved.
 *
 * Wiki resources live in folders under a page, and a person names a folder
 * when moving a resource or when looking inside one. A path naming the
 * parent folder repeatedly could otherwise reach the rest of the disk, so
 * these cases pin down that the reduction resolves those steps rather than
 * striking the characters out of the text, which leaves ways through.
 *
 * @author Chris Pollett
 */
class ControllerTest extends UnitTest
{
    /**
     * A controller whose inherited cleaning is under test.
     * @var StaticController
     */
    public $controller;
    /**
     * Builds a controller without running its constructor, since the
     * cleaning works on a value handed to it and reads neither disk nor
     * database.
     */
    public function setUp()
    {
        $reflection = new \ReflectionClass(StaticController::class);
        $this->controller = $reflection->newInstanceWithoutConstructor();
    }
    /**
     * Lets go of the controller so one case does not leak into the next.
     */
    public function tearDown()
    {
        $this->controller = null;
    }
    /**
     * An ordinary path is left as it stands, apart from writing every
     * separator the one way and dropping empty steps.
     */
    public function ordinaryPathTestCase()
    {
        $this->assertEqual(Controller::normalizePath("docs/images"),
            "docs/images", "a plain path is unchanged");
        $this->assertEqual(Controller::normalizePath("docs\\\\images"),
            "docs/images", "a backslash is written as a separator");
        $this->assertEqual(Controller::normalizePath("/docs//images/"),
            "docs/images", "empty steps and edge separators are dropped");
        $this->assertEqual(Controller::normalizePath("docs/./images"),
            "docs/images", "a step naming the folder itself is dropped");
    }
    /**
     * A step naming the parent folder removes the step before it, and one
     * with nothing before it is dropped, so no path reaches above where it
     * starts.
     */
    public function parentStepsResolveTestCase()
    {
        $this->assertEqual(Controller::normalizePath("docs/../images"),
            "images", "a parent step removes the folder before it");
        $this->assertEqual(Controller::normalizePath("../../etc"), "etc",
            "parent steps at the start have nothing to remove");
        $this->assertEqual(Controller::normalizePath("a/b/../../../c"), "c",
            "more parent steps than folders still cannot reach outside");
        $this->assertEqual(Controller::normalizePath(".."), "",
            "a path that is only a parent step comes back empty");
    }
    /**
     * Asking the cleaner for a path gives back the reduced form, so a
     * caller reading a folder name out of a request need not remember to
     * reduce it separately.
     */
    public function cleanPathTypeTestCase()
    {
        $this->assertEqual($this->controller->clean("docs/../images",
            "path"), "images", "the cleaner reduces a path it is given");
        $this->assertEqual($this->controller->clean("../../etc", "path"),
            "etc", "the cleaner leaves no way above where it starts");
        $this->assertEqual($this->controller->clean("docs/images", "path"),
            "docs/images", "a plain path comes back unchanged");
    }
    /**
     * Paths written to defeat striking the two-dot sequence out of the text
     * are reduced to names that stay inside, since the reduction reads whole
     * steps rather than characters.
     */
    public function textStrippingDefeatsTestCase()
    {
        $this->assertEqual(Controller::normalizePath("....//etc"),
            "..../etc", "four dots name a folder, not two parent steps");
        $this->assertEqual(Controller::normalizePath(".../....//secret"),
            ".../..../secret", "runs of dots are ordinary folder names");
        $this->assertTrue(
            strpos(Controller::normalizePath("../../../../etc/passwd"),
            "..") === false, "no parent step survives the reduction");
    }
}
X