v2.1.0
This commit is contained in:
@@ -12,6 +12,7 @@ use Firebase\JWT\JWT;
|
||||
use Objects\DatabaseEntity\Language;
|
||||
use Objects\DatabaseEntity\Session;
|
||||
use Objects\DatabaseEntity\User;
|
||||
use Objects\Router\Router;
|
||||
|
||||
class Context {
|
||||
|
||||
@@ -20,12 +21,14 @@ class Context {
|
||||
private ?User $user;
|
||||
private Configuration $configuration;
|
||||
private Language $language;
|
||||
public ?Router $router;
|
||||
|
||||
public function __construct() {
|
||||
|
||||
$this->sql = null;
|
||||
$this->session = null;
|
||||
$this->user = null;
|
||||
$this->router = null;
|
||||
$this->configuration = new Configuration();
|
||||
$this->setLanguage(Language::DEFAULT_LANGUAGE());
|
||||
|
||||
|
||||
18
core/Objects/CustomTwigFunctions.class.php
Normal file
18
core/Objects/CustomTwigFunctions.class.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Objects;
|
||||
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class CustomTwigFunctions extends AbstractExtension {
|
||||
public function getFunctions(): array {
|
||||
return [
|
||||
new TwigFunction('L', array($this, 'translate')),
|
||||
];
|
||||
}
|
||||
|
||||
public function translate(string $key): string {
|
||||
return L($key);
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,18 @@ abstract class DatabaseEntity {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function insert(SQL $sql): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
$res = $handler->insert($this);
|
||||
if ($res === false) {
|
||||
return false;
|
||||
} else if ($this->id === null) {
|
||||
$this->id = $res;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(SQL $sql): bool {
|
||||
$handler = self::getHandler($sql);
|
||||
if ($this->id === null) {
|
||||
|
||||
@@ -311,10 +311,7 @@ class DatabaseEntityHandler {
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
|
||||
$id = $entity->getId();
|
||||
$action = $id === null ? "insert" : "update";
|
||||
|
||||
private function prepareRow(DatabaseEntity $entity, string $action, ?array $columns = null) {
|
||||
$row = [];
|
||||
foreach ($this->columns as $propertyName => $column) {
|
||||
if ($columns && !in_array($column->getName(), $columns)) {
|
||||
@@ -331,7 +328,10 @@ class DatabaseEntityHandler {
|
||||
} else if (!$this->columns[$propertyName]->notNull()) {
|
||||
$value = null;
|
||||
} else {
|
||||
if ($action !== "update") {
|
||||
$defaultValue = self::getAttribute($property, DefaultValue::class);
|
||||
if ($defaultValue) {
|
||||
$value = $defaultValue->getValue();
|
||||
} else if ($action !== "update") {
|
||||
$this->logger->error("Cannot $action entity: property '$propertyName' was not initialized yet.");
|
||||
return false;
|
||||
} else {
|
||||
@@ -342,29 +342,62 @@ class DatabaseEntityHandler {
|
||||
$row[$column->getName()] = $value;
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function update(DatabaseEntity $entity, ?array $columns = null) {
|
||||
$row = $this->prepareRow($entity, "update", $columns);
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entity->preInsert($row);
|
||||
$query = $this->sql->update($this->tableName)
|
||||
->where(new Compare($this->tableName . ".id", $entity->getId()));
|
||||
|
||||
foreach ($row as $columnName => $value) {
|
||||
$query->set($columnName, $value);
|
||||
}
|
||||
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
public function insert(DatabaseEntity $entity) {
|
||||
$row = $this->prepareRow($entity, "insert");
|
||||
if ($row === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$entity->preInsert($row);
|
||||
|
||||
// insert with id?
|
||||
$entityId = $entity->getId();
|
||||
if ($entityId !== null) {
|
||||
$row["id"] = $entityId;
|
||||
}
|
||||
|
||||
if ($id === null) {
|
||||
$res = $this->sql->insert($this->tableName, array_keys($row))
|
||||
->addRow(...array_values($row))
|
||||
->returning("id")
|
||||
->execute();
|
||||
$query = $this->sql->insert($this->tableName, array_keys($row))
|
||||
->addRow(...array_values($row));
|
||||
|
||||
if ($res !== false) {
|
||||
return $this->sql->getLastInsertId();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// return id if its auto-generated
|
||||
if ($entityId === null) {
|
||||
$query->returning("id");
|
||||
}
|
||||
|
||||
$res = $query->execute();
|
||||
if ($res !== false) {
|
||||
return $this->sql->getLastInsertId();
|
||||
} else {
|
||||
$query = $this->sql->update($this->tableName)
|
||||
->where(new Compare($this->tableName . ".id", $id));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($row as $columnName => $value) {
|
||||
$query->set($columnName, $value);
|
||||
}
|
||||
|
||||
return $query->execute();
|
||||
public function insertOrUpdate(DatabaseEntity $entity, ?array $columns = null) {
|
||||
$id = $entity->getId();
|
||||
if ($id === null) {
|
||||
return $this->insert($entity);
|
||||
} else {
|
||||
return $this->update($entity, $columns);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Objects\DatabaseEntity {
|
||||
|
||||
use Driver\SQL\SQL;
|
||||
use Objects\DatabaseEntity\Attribute\MaxLength;
|
||||
use Objects\DatabaseEntity\Attribute\Transient;
|
||||
use Objects\lang\LanguageModule;
|
||||
@@ -14,15 +15,13 @@ namespace Objects\DatabaseEntity {
|
||||
#[MaxLength(5)] private string $code;
|
||||
#[MaxLength(32)] private string $name;
|
||||
|
||||
#[Transient] private array $modules;
|
||||
#[Transient] protected array $entries;
|
||||
#[Transient] private array $modules = [];
|
||||
#[Transient] protected array $entries = [];
|
||||
|
||||
public function __construct(int $id, string $code, string $name) {
|
||||
parent::__construct($id);
|
||||
$this->code = $code;
|
||||
$this->name = $name;
|
||||
$this->entries = array();
|
||||
$this->modules = array();
|
||||
}
|
||||
|
||||
public function getCode(): string {
|
||||
@@ -42,9 +41,11 @@ namespace Objects\DatabaseEntity {
|
||||
$module = new $module();
|
||||
}
|
||||
|
||||
$moduleEntries = $module->getEntries($this->code);
|
||||
$this->entries = array_merge($this->entries, $moduleEntries);
|
||||
$this->modules[] = $module;
|
||||
if (!in_array($module, $this->modules)) {
|
||||
$moduleEntries = $module->getEntries($this->code);
|
||||
$this->entries = array_merge($this->entries, $moduleEntries);
|
||||
$this->modules[] = $module;
|
||||
}
|
||||
}
|
||||
|
||||
public function translate(string $key): string {
|
||||
@@ -89,6 +90,10 @@ namespace Objects\DatabaseEntity {
|
||||
|
||||
return new Language(1, "en_US", "American English");
|
||||
}
|
||||
|
||||
public function getEntries(): array {
|
||||
return $this->entries;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,12 +105,6 @@ class Session extends DatabaseEntity {
|
||||
);
|
||||
}
|
||||
|
||||
public function insert(bool $stayLoggedIn = false): bool {
|
||||
$this->stayLoggedIn = $stayLoggedIn;
|
||||
$this->active = true;
|
||||
return $this->update();
|
||||
}
|
||||
|
||||
public function destroy(): bool {
|
||||
session_destroy();
|
||||
$this->active = false;
|
||||
|
||||
@@ -6,6 +6,8 @@ use Api\Parameter\Parameter;
|
||||
|
||||
abstract class AbstractRoute {
|
||||
|
||||
const PARAMETER_PATTERN = "/^{([^:]+)(:(.*?)(\?)?)?}$/";
|
||||
|
||||
private string $pattern;
|
||||
private bool $exact;
|
||||
|
||||
@@ -64,7 +66,7 @@ abstract class AbstractRoute {
|
||||
|
||||
$params = [];
|
||||
for (; $patternOffset < $countPattern; $patternOffset++) {
|
||||
if (!preg_match("/^{([^:]+)(:(.*?)(\?)?)?}$/", $patternParts[$patternOffset], $match)) {
|
||||
if (!preg_match(self::PARAMETER_PATTERN, $patternParts[$patternOffset], $match)) {
|
||||
|
||||
// not a parameter? check if it matches
|
||||
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
||||
@@ -122,4 +124,30 @@ abstract class AbstractRoute {
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function getUrl(array $parameters = []): string {
|
||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||
|
||||
foreach ($patternParts as $i => $part) {
|
||||
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||
$paramName = $match[1];
|
||||
$patternParts[$i] = $parameters[$paramName] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return "/" . implode("/", array_filter($patternParts));
|
||||
}
|
||||
|
||||
public function getParameterNames(): array {
|
||||
$parameterNames = [];
|
||||
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||||
|
||||
foreach ($patternParts as $part) {
|
||||
if (preg_match(self::PARAMETER_PATTERN, $part, $match)) {
|
||||
$parameterNames[] = $match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $parameterNames;
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class ApiRoute extends AbstractRoute {
|
||||
if (empty($params["endpoint"])) {
|
||||
header("Content-Type: text/html");
|
||||
$document = new \Elements\TemplateDocument($router, "swagger.twig");
|
||||
return $document->getCode();
|
||||
return $document->load();
|
||||
} else if (!preg_match("/[a-zA-Z]+/", $params["endpoint"])) {
|
||||
http_response_code(400);
|
||||
$response = createError("Invalid Method");
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
namespace Objects\Router;
|
||||
|
||||
use Elements\Document;
|
||||
use Objects\Context;
|
||||
use Objects\Search\Searchable;
|
||||
use Objects\Search\SearchQuery;
|
||||
use ReflectionException;
|
||||
|
||||
class DocumentRoute extends AbstractRoute {
|
||||
|
||||
use Searchable;
|
||||
|
||||
private string $className;
|
||||
private array $args;
|
||||
private ?\ReflectionClass $reflectionClass;
|
||||
@@ -31,7 +36,7 @@ class DocumentRoute extends AbstractRoute {
|
||||
}
|
||||
} catch (ReflectionException $exception) {
|
||||
$this->reflectionClass = null;
|
||||
return false;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->reflectionClass = null;
|
||||
@@ -55,16 +60,32 @@ class DocumentRoute extends AbstractRoute {
|
||||
}
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
if (!$this->loadClass()) {
|
||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||||
}
|
||||
|
||||
try {
|
||||
$args = array_merge([$router], $this->args);
|
||||
if (!$this->loadClass()) {
|
||||
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||||
}
|
||||
|
||||
$args = array_merge([$router], $this->args, $params);
|
||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||
return $document->getCode($params);
|
||||
return $document->load($params);
|
||||
} catch (\ReflectionException $e) {
|
||||
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
public function doSearch(Context $context, SearchQuery $query): array {
|
||||
try {
|
||||
if ($this->loadClass()) {
|
||||
$args = array_merge([$context->router], $this->args);
|
||||
$document = $this->reflectionClass->newInstanceArgs($args);
|
||||
if ($document->isSearchable()) {
|
||||
return $document->doSearch($query, $this);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (\ReflectionException) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ class Router {
|
||||
|
||||
private Context $context;
|
||||
private Logger $logger;
|
||||
private ?AbstractRoute $activeRoute;
|
||||
private ?string $requestedUri;
|
||||
protected array $routes;
|
||||
protected array $statusCodeRoutes;
|
||||
|
||||
@@ -16,6 +18,9 @@ class Router {
|
||||
$this->context = $context;
|
||||
$this->routes = [];
|
||||
$this->statusCodeRoutes = [];
|
||||
$this->activeRoute = null;
|
||||
$this->requestedUri = null;
|
||||
$this->context->router = $this;
|
||||
|
||||
$sql = $context->getSQL();
|
||||
if ($sql) {
|
||||
@@ -26,14 +31,24 @@ class Router {
|
||||
}
|
||||
}
|
||||
|
||||
public function getActiveRoute(): ?AbstractRoute {
|
||||
return $this->activeRoute;
|
||||
}
|
||||
|
||||
public function getRequestedUri(): ?string {
|
||||
return $this->requestedUri;
|
||||
}
|
||||
|
||||
public function run(string $url): string {
|
||||
|
||||
// TODO: do we want a global try cache and return status page 500 on any error?
|
||||
$this->requestedUri = $url;
|
||||
|
||||
$url = strtok($url, "?");
|
||||
foreach ($this->routes as $route) {
|
||||
$pathParams = $route->match($url);
|
||||
if ($pathParams !== false) {
|
||||
$this->activeRoute = $route;
|
||||
return $route->call($this, $pathParams);
|
||||
}
|
||||
}
|
||||
@@ -54,7 +69,6 @@ class Router {
|
||||
if ($res) {
|
||||
return $req->getResult()["html"];
|
||||
} else {
|
||||
var_dump($req->getLastError());
|
||||
$description = htmlspecialchars($params["status_description"]);
|
||||
return "<b>$code - $description</b>";
|
||||
}
|
||||
@@ -62,11 +76,11 @@ class Router {
|
||||
}
|
||||
|
||||
public function addRoute(AbstractRoute $route) {
|
||||
if (preg_match("/^\d+$/", $route->getPattern())) {
|
||||
$this->statusCodeRoutes[$route->getPattern()] = $route;
|
||||
} else {
|
||||
$this->routes[] = $route;
|
||||
if (preg_match("/^\/(\d+)$/", $route->getPattern(), $re)) {
|
||||
$this->statusCodeRoutes[$re[1]] = $route;
|
||||
}
|
||||
|
||||
$this->routes[] = $route;
|
||||
}
|
||||
|
||||
public function writeCache(string $file): bool {
|
||||
@@ -134,4 +148,15 @@ class RouterCache extends Router {
|
||||
// strip leading slash
|
||||
return preg_replace("/^\/+/", "", $url);
|
||||
}
|
||||
|
||||
public function getRoutes(bool $includeStatusRoutes = false): array {
|
||||
|
||||
if (!$includeStatusRoutes && !empty($this->statusCodeRoutes)) {
|
||||
return array_filter($this->routes, function ($route) {
|
||||
return !in_array($route, $this->statusCodeRoutes);
|
||||
});
|
||||
}
|
||||
|
||||
return $this->routes;
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
namespace Objects\Router;
|
||||
|
||||
use Objects\Context;
|
||||
use Objects\Search\Searchable;
|
||||
use Objects\Search\SearchQuery;
|
||||
use Objects\Search\SearchResult;
|
||||
|
||||
class StaticFileRoute extends AbstractRoute {
|
||||
|
||||
use Searchable;
|
||||
|
||||
private string $path;
|
||||
private int $code;
|
||||
|
||||
@@ -15,7 +22,7 @@ class StaticFileRoute extends AbstractRoute {
|
||||
|
||||
public function call(Router $router, array $params): string {
|
||||
http_response_code($this->code);
|
||||
$this->serveStatic($this->path, $router);
|
||||
$this->serveStatic($this->getAbsolutePath(), $router);
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -23,9 +30,11 @@ class StaticFileRoute extends AbstractRoute {
|
||||
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
||||
}
|
||||
|
||||
public static function serveStatic(string $path, ?Router $router = null) {
|
||||
public function getAbsolutePath(): string {
|
||||
return WEBROOT . DIRECTORY_SEPARATOR . $this->path;
|
||||
}
|
||||
|
||||
$path = realpath(WEBROOT . DIRECTORY_SEPARATOR . $path);
|
||||
public static function serveStatic(string $path, ?Router $router = null) {
|
||||
if (!startsWith($path, WEBROOT . DIRECTORY_SEPARATOR)) {
|
||||
http_response_code(406);
|
||||
echo "<b>Access restricted, requested file outside web root:</b> " . htmlspecialchars($path);
|
||||
@@ -72,4 +81,34 @@ class StaticFileRoute extends AbstractRoute {
|
||||
downloadFile($handle, $offset, $length);
|
||||
}
|
||||
}
|
||||
|
||||
public function doSearch(Context $context, SearchQuery $query): array {
|
||||
|
||||
$results = [];
|
||||
$path = $this->getAbsolutePath();
|
||||
if (is_file($path) && is_readable($path)) {
|
||||
$pathInfo = pathinfo($path);
|
||||
$extension = $pathInfo["extension"] ?? "";
|
||||
$fileName = $pathInfo["filename"] ?? "";
|
||||
if ($context->getSettings()->isExtensionAllowed($extension)) {
|
||||
$mimeType = mime_content_type($path);
|
||||
if (startsWith($mimeType, "text/")) {
|
||||
$document = @file_get_contents($path);
|
||||
if ($document) {
|
||||
if ($mimeType === "text/html") {
|
||||
$results = Searchable::searchHtml($document, $query);
|
||||
} else {
|
||||
$results = Searchable::searchText($document, $query);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$results = array_map(function ($res) use ($fileName) {
|
||||
return new SearchResult($this->getPattern(), $fileName, $res["text"]);
|
||||
}, $results);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
19
core/Objects/Search/SearchQuery.class.php
Normal file
19
core/Objects/Search/SearchQuery.class.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\Search;
|
||||
|
||||
class SearchQuery {
|
||||
|
||||
private string $query;
|
||||
private array $parts;
|
||||
|
||||
public function __construct(string $query) {
|
||||
$this->query = $query;
|
||||
$this->parts = array_unique(array_filter(explode(" ", strtolower($query))));
|
||||
}
|
||||
|
||||
public function getQuery(): string {
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
}
|
||||
30
core/Objects/Search/SearchResult.class.php
Normal file
30
core/Objects/Search/SearchResult.class.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\Search;
|
||||
|
||||
use Objects\ApiObject;
|
||||
|
||||
class SearchResult extends ApiObject {
|
||||
|
||||
private string $url;
|
||||
private string $title;
|
||||
private string $text;
|
||||
|
||||
public function __construct(string $url, string $title, string $text) {
|
||||
$this->url = $url;
|
||||
$this->title = $title;
|
||||
$this->text = $text;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
"url" => $this->url,
|
||||
"title" => $this->title,
|
||||
"text" => $this->text
|
||||
];
|
||||
}
|
||||
|
||||
public function setUrl(string $url) {
|
||||
$this->url = $url;
|
||||
}
|
||||
}
|
||||
91
core/Objects/Search/Searchable.trait.php
Normal file
91
core/Objects/Search/Searchable.trait.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Objects\Search;
|
||||
|
||||
use Html2Text\Html2Text;
|
||||
use Objects\Context;
|
||||
use function PHPUnit\Framework\stringContains;
|
||||
|
||||
trait Searchable {
|
||||
|
||||
public static function searchArray(array $arr, SearchQuery $query): array {
|
||||
$results = [];
|
||||
foreach ($arr as $key => $value) {
|
||||
$results = array_merge($results, self::searchHtml($key, $query));
|
||||
if (is_array($value)) {
|
||||
$results = array_merge($results, self::searchArray($value, $query));
|
||||
} else {
|
||||
$results = array_merge($results, self::searchHtml(strval($value), $query));
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public abstract function doSearch(Context $context, SearchQuery $query): array;
|
||||
|
||||
public static function searchHtml(string $document, SearchQuery $query): array {
|
||||
if (stringContains($document, "<")) {
|
||||
$converter = new Html2Text($document);
|
||||
$text = trim($converter->getText());
|
||||
} else {
|
||||
$text = $document;
|
||||
}
|
||||
|
||||
$text = trim(preg_replace('!\s+!', ' ', $text));
|
||||
return self::searchText($text, $query);
|
||||
}
|
||||
|
||||
public static function searchText(string $content, SearchQuery $query): array {
|
||||
$offset = 0;
|
||||
$searchTerm = $query->getQuery();
|
||||
$stringLength = strlen($searchTerm);
|
||||
$contentLength = strlen($content);
|
||||
$lastPos = 0;
|
||||
|
||||
$results = [];
|
||||
do {
|
||||
$pos = stripos($content, $searchTerm, $offset);
|
||||
if ($pos !== false) {
|
||||
if ($lastPos === 0 || $pos > $lastPos + 192 + $stringLength) {
|
||||
$extract = self::viewExtract($content, $pos, $searchTerm);
|
||||
$results[] = array(
|
||||
"text" => $extract,
|
||||
"pos" => $pos,
|
||||
"lastPos" => $lastPos
|
||||
);
|
||||
$lastPos = $pos;
|
||||
}
|
||||
|
||||
$offset = $pos + $stringLength;
|
||||
}
|
||||
} while ($pos !== false && $offset < $contentLength);
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
private static function viewExtract(string $content, int $pos, $string): array|string|null {
|
||||
$length = strlen($string);
|
||||
$start = max(0, $pos - 32);
|
||||
$end = min(strlen($content) - 1, $pos + $length + 192);
|
||||
|
||||
if ($start > 0) {
|
||||
if (!ctype_space($content[$start - 1]) &&
|
||||
!ctype_space($content[$start])) {
|
||||
$start = $start + strpos(substr($content, $start, $end), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
if ($end < strlen($content) - 1) {
|
||||
if (!ctype_space($content[$end + 1]) &&
|
||||
!ctype_space($content[$end])) {
|
||||
$end = $start + strrpos(substr($content, $start, $end - $start), ' ');
|
||||
}
|
||||
}
|
||||
|
||||
$extract = trim(substr($content, $start, $end - $start + 1));
|
||||
if ($start > 0) $extract = ".. " . $extract;
|
||||
if ($end < strlen($content) - 1) $extract .= " ..";
|
||||
return preg_replace("/" . preg_quote($string) . "(?=[^>]*(<|$))/i", "<span class=\"highlight\">\$0</span>", $extract);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user