commit
e6754f2154
208 changed files with 8987 additions and 2009 deletions
|
@ -17,27 +17,13 @@ http {
|
||||||
index index.html index.php;
|
index index.html index.php;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
root /var/www/shaarli;
|
root /var/www/shaarli;
|
||||||
|
|
||||||
access_log /var/log/nginx/shaarli.access.log;
|
access_log /var/log/nginx/shaarli.access.log;
|
||||||
error_log /var/log/nginx/shaarli.error.log;
|
error_log /var/log/nginx/shaarli.error.log;
|
||||||
|
|
||||||
location ~ /\. {
|
location ~* \.(?:ico|css|js|gif|jpe?g|png|ttf|oet|woff2?)$ {
|
||||||
# deny access to dotfiles
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ~$ {
|
|
||||||
# deny access to temp editor files, e.g. "script.php~"
|
|
||||||
access_log off;
|
|
||||||
log_not_found off;
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(?:ico|css|js|gif|jpe?g|png)$ {
|
|
||||||
# cache static assets
|
# cache static assets
|
||||||
expires max;
|
expires max;
|
||||||
add_header Pragma public;
|
add_header Pragma public;
|
||||||
|
@ -49,25 +35,25 @@ http {
|
||||||
alias /var/www/shaarli/images/favicon.ico;
|
alias /var/www/shaarli/images/favicon.ico;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location /doc/html/ {
|
||||||
# Slim - rewrite URLs
|
default_type "text/html";
|
||||||
try_files $uri /index.php$is_args$args;
|
try_files $uri $uri/ $uri.html =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ (index)\.php$ {
|
location / {
|
||||||
|
# Slim - rewrite URLs & do NOT serve static files through this location
|
||||||
|
try_files _ /index.php$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ index\.php$ {
|
||||||
# Slim - split URL path into (script_filename, path_info)
|
# Slim - split URL path into (script_filename, path_info)
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(index.php)(/.+)$;
|
||||||
|
|
||||||
# filter and proxy PHP requests to PHP-FPM
|
# filter and proxy PHP requests to PHP-FPM
|
||||||
fastcgi_pass unix:/var/run/php-fpm.sock;
|
fastcgi_pass unix:/var/run/php-fpm.sock;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
include fastcgi.conf;
|
include fastcgi.conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ \.php$ {
|
|
||||||
# deny access to all other PHP scripts
|
|
||||||
deny all;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,16 @@
|
||||||
.dev
|
.dev
|
||||||
.git
|
.git
|
||||||
.github
|
.github
|
||||||
|
.gitattributes
|
||||||
|
.gitignore
|
||||||
|
.travis.yml
|
||||||
tests
|
tests
|
||||||
|
|
||||||
|
# Docker related resources are not needed inside the container
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
Dockerfile.armhf
|
||||||
|
|
||||||
# Docker Compose resources
|
# Docker Compose resources
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
|
||||||
|
@ -13,6 +21,9 @@ data/*
|
||||||
pagecache/*
|
pagecache/*
|
||||||
tmp/*
|
tmp/*
|
||||||
|
|
||||||
|
# Shaarli's docs are created during the build
|
||||||
|
doc/html/
|
||||||
|
|
||||||
# Eclipse project files
|
# Eclipse project files
|
||||||
.settings
|
.settings
|
||||||
.buildpath
|
.buildpath
|
||||||
|
|
|
@ -13,7 +13,7 @@ RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
|
||||||
# Alternative (if the 2 lines above don't work)
|
# Alternative (if the 2 lines above don't work)
|
||||||
# SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
|
# SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
|
||||||
|
|
||||||
# REST API
|
# Slim URL Redirection
|
||||||
# Ionos Hosting needs RewriteBase /
|
# Ionos Hosting needs RewriteBase /
|
||||||
# RewriteBase /
|
# RewriteBase /
|
||||||
RewriteCond %{REQUEST_FILENAME} !-f
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
|
|
@ -49,6 +49,10 @@ cache:
|
||||||
directories:
|
directories:
|
||||||
- $HOME/.composer/cache
|
- $HOME/.composer/cache
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
# Disable xdebug: it significantly speed up tests and linter, and we don't use coverage yet
|
||||||
|
- phpenv config-rm xdebug.ini || echo 'No xdebug config.'
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# install/update composer and php dependencies
|
# install/update composer and php dependencies
|
||||||
- composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION
|
- composer config --unset platform && composer config platform.php $TRAVIS_PHP_VERSION
|
||||||
|
@ -60,4 +64,5 @@ before_script:
|
||||||
script:
|
script:
|
||||||
- make clean
|
- make clean
|
||||||
- make check_permissions
|
- make check_permissions
|
||||||
|
- make code_sniffer
|
||||||
- make all_tests
|
- make all_tests
|
||||||
|
|
5
AUTHORS
5
AUTHORS
|
@ -1,4 +1,4 @@
|
||||||
991 ArthurHoaro <arthur@hoa.ro>
|
1097 ArthurHoaro <arthur@hoa.ro>
|
||||||
402 VirtualTam <virtualtam@flibidi.net>
|
402 VirtualTam <virtualtam@flibidi.net>
|
||||||
294 nodiscc <nodiscc@gmail.com>
|
294 nodiscc <nodiscc@gmail.com>
|
||||||
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
|
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
|
||||||
|
@ -25,6 +25,7 @@
|
||||||
2 Alexandre G.-Raymond <alex@ndre.gr>
|
2 Alexandre G.-Raymond <alex@ndre.gr>
|
||||||
2 Chris Kuethe <chris.kuethe@gmail.com>
|
2 Chris Kuethe <chris.kuethe@gmail.com>
|
||||||
2 Felix Bartels <felix@host-consultants.de>
|
2 Felix Bartels <felix@host-consultants.de>
|
||||||
|
2 Ganesh Kandu <kanduganesh@gmail.com>
|
||||||
2 Guillaume Virlet <github@virlet.org>
|
2 Guillaume Virlet <github@virlet.org>
|
||||||
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
|
2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
|
||||||
2 Mathieu Chabanon <git@matchab.fr>
|
2 Mathieu Chabanon <git@matchab.fr>
|
||||||
|
@ -39,6 +40,7 @@
|
||||||
2 pips <pips@e5150.fr>
|
2 pips <pips@e5150.fr>
|
||||||
2 trailjeep <trailjeep@gmail.com>
|
2 trailjeep <trailjeep@gmail.com>
|
||||||
2 yude <yudesleepy@gmail.com>
|
2 yude <yudesleepy@gmail.com>
|
||||||
|
2 yudete <yu@yude.moe>
|
||||||
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
|
1 Adrien Oliva <adrien.oliva@yapbreak.fr>
|
||||||
1 Adrien le Maire <adrien@alemaire.be>
|
1 Adrien le Maire <adrien@alemaire.be>
|
||||||
1 Alexis J <alexis@effingo.be>
|
1 Alexis J <alexis@effingo.be>
|
||||||
|
@ -65,6 +67,7 @@
|
||||||
1 Kevin Masson <kevin.masson@methodinthemadness.eu>
|
1 Kevin Masson <kevin.masson@methodinthemadness.eu>
|
||||||
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
|
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
|
||||||
1 Lionel Martin <renarddesmers@gmail.com>
|
1 Lionel Martin <renarddesmers@gmail.com>
|
||||||
|
1 Loïc Carr <zizou.xena@gmail.com>
|
||||||
1 Mark Gerarts <mark.gerarts@gmail.com>
|
1 Mark Gerarts <mark.gerarts@gmail.com>
|
||||||
1 Marsup <marsup@gmail.com>
|
1 Marsup <marsup@gmail.com>
|
||||||
1 Paul van den Burg <github@paulvandenburg.nl>
|
1 Paul van den Burg <github@paulvandenburg.nl>
|
||||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -4,7 +4,55 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## [v0.12.1]() - UNRELEASED
|
## [v0.12.2]() - UNRELEASED
|
||||||
|
|
||||||
|
## [v0.12.1](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-11-12
|
||||||
|
|
||||||
|
> nginx ([#1628](https://github.com/shaarli/Shaarli/pull/1628)) and Apache ([#1630](https://github.com/shaarli/Shaarli/pull/1630)) configurations have been reviewed. It is recommended that you
|
||||||
|
> update yours using [the documentation](https://shaarli.readthedocs.io/en/master/Server-configuration/).
|
||||||
|
> Users using official Docker image will receive updated configuration automatically.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Bulk creation of bookmarks
|
||||||
|
- Server administration tool page (and install page requirements)
|
||||||
|
- Support any tag separator, not just whitespaces
|
||||||
|
- Share a private bookmark using a URL with a token
|
||||||
|
- Add a setting to retrieve bookmark metadata asynchronously (enabled by default)
|
||||||
|
- Highlight fulltext search results
|
||||||
|
- Weekly and monthly view/RSS feed for daily page
|
||||||
|
- MarkdownExtra formatter
|
||||||
|
- Default formatter: add a setting to disable auto-linkification
|
||||||
|
- Add mutex on datastore I/O operations to prevent data loss
|
||||||
|
- PHP 8.0 support
|
||||||
|
- REST API: allow override of creation and update dates
|
||||||
|
- Add strict types for bookmarks management
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improve regex and performances to extract HTML metadata (title, description, etc.)
|
||||||
|
- Support using Shaarli without URL rewriting (prefix URL with `/index.php/`)
|
||||||
|
- Improve the "Manage tags" tools page
|
||||||
|
- Use PSR-3 logger for login attempts
|
||||||
|
- Move utils classes to Shaarli\Helper namespace and folder
|
||||||
|
- Include php-simplexml in Docker image
|
||||||
|
- Raise 404 error instead of 500 if permalink access is denied
|
||||||
|
- Display error details even with dev.debug set to false
|
||||||
|
- Reviewed nginx configuration
|
||||||
|
- Reviewed Apache configuration
|
||||||
|
- Replace vimeo link in demo bookmarks due to IP ban on the demo instance
|
||||||
|
- Apply PSR-12 on code base, and add CI check using PHPCS
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Compatiliby issue on login with PHP 7.1
|
||||||
|
- Japanese translations update
|
||||||
|
- Redirect to referrer after bookmark deletion
|
||||||
|
- Inject ROOT_PATH in plugin instead of regenerating it everywhere
|
||||||
|
- Wallabag plugin: minor improvements
|
||||||
|
- REST API postLink: change relative path to absolute path
|
||||||
|
- Webpack: fix vintage theme images include
|
||||||
|
- Docker-compose: fix SSL certificate + add parameter for Docker tag
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `config.json.php` new lines in prefix/suffix to prevent issues with Windows PHP
|
||||||
|
|
||||||
## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13
|
## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ RUN cd shaarli \
|
||||||
|
|
||||||
# Stage 4:
|
# Stage 4:
|
||||||
# - Shaarli image
|
# - Shaarli image
|
||||||
FROM alpine:3.8
|
FROM alpine:3.12
|
||||||
LABEL maintainer="Shaarli Community"
|
LABEL maintainer="Shaarli Community"
|
||||||
|
|
||||||
RUN apk --update --no-cache add \
|
RUN apk --update --no-cache add \
|
||||||
|
@ -44,6 +44,7 @@ RUN apk --update --no-cache add \
|
||||||
php7-openssl \
|
php7-openssl \
|
||||||
php7-session \
|
php7-session \
|
||||||
php7-xml \
|
php7-xml \
|
||||||
|
php7-simplexml \
|
||||||
php7-zlib \
|
php7-zlib \
|
||||||
s6
|
s6
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Stage 1:
|
# Stage 1:
|
||||||
# - Copy Shaarli sources
|
# - Copy Shaarli sources
|
||||||
# - Build documentation
|
# - Build documentation
|
||||||
FROM arm32v6/alpine:3.8 as docs
|
FROM arm32v6/alpine:3.10 as docs
|
||||||
ADD . /usr/src/app/shaarli
|
ADD . /usr/src/app/shaarli
|
||||||
RUN apk --update --no-cache add py2-pip \
|
RUN apk --update --no-cache add py2-pip \
|
||||||
&& cd /usr/src/app/shaarli \
|
&& cd /usr/src/app/shaarli \
|
||||||
|
@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \
|
||||||
|
|
||||||
# Stage 2:
|
# Stage 2:
|
||||||
# - Resolve PHP dependencies with Composer
|
# - Resolve PHP dependencies with Composer
|
||||||
FROM arm32v6/alpine:3.8 as composer
|
FROM arm32v6/alpine:3.10 as composer
|
||||||
COPY --from=docs /usr/src/app/shaarli /app/shaarli
|
COPY --from=docs /usr/src/app/shaarli /app/shaarli
|
||||||
RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
|
RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
|
||||||
&& cd /app/shaarli \
|
&& cd /app/shaarli \
|
||||||
|
@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer
|
||||||
|
|
||||||
# Stage 3:
|
# Stage 3:
|
||||||
# - Frontend dependencies
|
# - Frontend dependencies
|
||||||
FROM arm32v6/alpine:3.8 as node
|
FROM arm32v6/alpine:3.10 as node
|
||||||
COPY --from=composer /app/shaarli /shaarli
|
COPY --from=composer /app/shaarli /shaarli
|
||||||
RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
|
RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
|
||||||
&& cd /shaarli \
|
&& cd /shaarli \
|
||||||
|
@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
|
||||||
|
|
||||||
# Stage 4:
|
# Stage 4:
|
||||||
# - Shaarli image
|
# - Shaarli image
|
||||||
FROM arm32v6/alpine:3.8
|
FROM arm32v6/alpine:3.10
|
||||||
LABEL maintainer="Shaarli Community"
|
LABEL maintainer="Shaarli Community"
|
||||||
|
|
||||||
RUN apk --update --no-cache add \
|
RUN apk --update --no-cache add \
|
||||||
|
|
5
Makefile
5
Makefile
|
@ -27,10 +27,6 @@ PHPCS := $(BIN)/phpcs
|
||||||
code_sniffer:
|
code_sniffer:
|
||||||
@$(PHPCS)
|
@$(PHPCS)
|
||||||
|
|
||||||
### - errors filtered by coding standard: PEAR, PSR1, PSR2, Zend...
|
|
||||||
PHPCS_%:
|
|
||||||
@$(PHPCS) --report-full --report-width=200 --standard=$*
|
|
||||||
|
|
||||||
### - errors by Git author
|
### - errors by Git author
|
||||||
code_sniffer_blame:
|
code_sniffer_blame:
|
||||||
@$(PHPCS) --report-gitblame
|
@$(PHPCS) --report-gitblame
|
||||||
|
@ -175,6 +171,7 @@ translate:
|
||||||
eslint:
|
eslint:
|
||||||
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
|
@yarn run eslint -c .dev/.eslintrc.js assets/vintage/js/
|
||||||
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
|
@yarn run eslint -c .dev/.eslintrc.js assets/default/js/
|
||||||
|
@yarn run eslint -c .dev/.eslintrc.js assets/common/js/
|
||||||
|
|
||||||
### Run CSSLint check against Shaarli's SCSS files
|
### Run CSSLint check against Shaarli's SCSS files
|
||||||
sasslint:
|
sasslint:
|
||||||
|
|
|
@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._
|
||||||
[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
|
[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
|
||||||
[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
|
[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
|
||||||
•
|
•
|
||||||
[![](https://img.shields.io/badge/latest-v0.12.0-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0)
|
[![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1)
|
||||||
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
|
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
|
||||||
•
|
•
|
||||||
[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
|
[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli;
|
namespace Shaarli;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Shaarli\Bookmark\Bookmark;
|
use Shaarli\Bookmark\Bookmark;
|
||||||
|
use Shaarli\Helper\FileUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class History
|
* Class History
|
||||||
|
@ -30,27 +32,27 @@ class History
|
||||||
/**
|
/**
|
||||||
* @var string Action key: a new link has been created.
|
* @var string Action key: a new link has been created.
|
||||||
*/
|
*/
|
||||||
const CREATED = 'CREATED';
|
public const CREATED = 'CREATED';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Action key: a link has been updated.
|
* @var string Action key: a link has been updated.
|
||||||
*/
|
*/
|
||||||
const UPDATED = 'UPDATED';
|
public const UPDATED = 'UPDATED';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Action key: a link has been deleted.
|
* @var string Action key: a link has been deleted.
|
||||||
*/
|
*/
|
||||||
const DELETED = 'DELETED';
|
public const DELETED = 'DELETED';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Action key: settings have been updated.
|
* @var string Action key: settings have been updated.
|
||||||
*/
|
*/
|
||||||
const SETTINGS = 'SETTINGS';
|
public const SETTINGS = 'SETTINGS';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Action key: a bulk import has been processed.
|
* @var string Action key: a bulk import has been processed.
|
||||||
*/
|
*/
|
||||||
const IMPORT = 'IMPORT';
|
public const IMPORT = 'IMPORT';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string History file path.
|
* @var string History file path.
|
||||||
|
|
|
@ -41,7 +41,7 @@ class Languages
|
||||||
/**
|
/**
|
||||||
* Core translations domain
|
* Core translations domain
|
||||||
*/
|
*/
|
||||||
const DEFAULT_DOMAIN = 'shaarli';
|
public const DEFAULT_DOMAIN = 'shaarli';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
|
@ -76,7 +76,8 @@ public function __construct($language, $conf)
|
||||||
$this->language = $confLanguage;
|
$this->language = $confLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! extension_loaded('gettext')
|
if (
|
||||||
|
! extension_loaded('gettext')
|
||||||
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
|
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
|
||||||
) {
|
) {
|
||||||
$this->initPhpTranslator();
|
$this->initPhpTranslator();
|
||||||
|
@ -98,7 +99,7 @@ protected function initGettextTranslator()
|
||||||
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
|
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
|
||||||
|
|
||||||
// Default extension translation from the current theme
|
// Default extension translation from the current theme
|
||||||
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language';
|
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language';
|
||||||
if (is_dir($themeTransFolder)) {
|
if (is_dir($themeTransFolder)) {
|
||||||
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
|
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
|
||||||
}
|
}
|
||||||
|
@ -121,7 +122,9 @@ protected function initPhpTranslator()
|
||||||
$translations = new Translations();
|
$translations = new Translations();
|
||||||
// Core translations
|
// Core translations
|
||||||
try {
|
try {
|
||||||
$translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
|
$translations = $translations->addFromPoFile(
|
||||||
|
'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'
|
||||||
|
);
|
||||||
$translations->setDomain('shaarli');
|
$translations->setDomain('shaarli');
|
||||||
$this->translator->loadTranslations($translations);
|
$this->translator->loadTranslations($translations);
|
||||||
} catch (\InvalidArgumentException $e) {
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
@ -129,11 +132,11 @@ protected function initPhpTranslator()
|
||||||
|
|
||||||
// Default extension translation from the current theme
|
// Default extension translation from the current theme
|
||||||
$theme = $this->conf->get('theme');
|
$theme = $this->conf->get('theme');
|
||||||
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language';
|
$themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language';
|
||||||
if (is_dir($themeTransFolder)) {
|
if (is_dir($themeTransFolder)) {
|
||||||
try {
|
try {
|
||||||
$translations = Translations::fromPoFile(
|
$translations = Translations::fromPoFile(
|
||||||
$themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po'
|
$themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po'
|
||||||
);
|
);
|
||||||
$translations->setDomain($theme);
|
$translations->setDomain($theme);
|
||||||
$this->translator->loadTranslations($translations);
|
$this->translator->loadTranslations($translations);
|
||||||
|
@ -149,7 +152,7 @@ protected function initPhpTranslator()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$extension = Translations::fromPoFile(
|
$extension = Translations::fromPoFile(
|
||||||
$translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'
|
$translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po'
|
||||||
);
|
);
|
||||||
$extension->setDomain($domain);
|
$extension->setDomain($domain);
|
||||||
$this->translator->loadTranslations($extension);
|
$this->translator->loadTranslations($extension);
|
||||||
|
@ -183,6 +186,7 @@ public static function getAvailableLanguages()
|
||||||
'en' => t('English'),
|
'en' => t('English'),
|
||||||
'fr' => t('French'),
|
'fr' => t('French'),
|
||||||
'jp' => t('Japanese'),
|
'jp' => t('Japanese'),
|
||||||
|
'ru' => t('Russian'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
*/
|
*/
|
||||||
class Thumbnailer
|
class Thumbnailer
|
||||||
{
|
{
|
||||||
const COMMON_MEDIA_DOMAINS = [
|
protected const COMMON_MEDIA_DOMAINS = [
|
||||||
'imgur.com',
|
'imgur.com',
|
||||||
'flickr.com',
|
'flickr.com',
|
||||||
'youtube.com',
|
'youtube.com',
|
||||||
|
@ -31,9 +31,9 @@ class Thumbnailer
|
||||||
'deviantart.com',
|
'deviantart.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
const MODE_ALL = 'all';
|
public const MODE_ALL = 'all';
|
||||||
const MODE_COMMON = 'common';
|
public const MODE_COMMON = 'common';
|
||||||
const MODE_NONE = 'none';
|
public const MODE_NONE = 'none';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var WebThumbnailer instance.
|
* @var WebThumbnailer instance.
|
||||||
|
@ -60,7 +60,7 @@ public function __construct($conf)
|
||||||
// TODO: create a proper error handling system able to catch exceptions...
|
// TODO: create a proper error handling system able to catch exceptions...
|
||||||
die(t(
|
die(t(
|
||||||
'php-gd extension must be loaded to use thumbnails. '
|
'php-gd extension must be loaded to use thumbnails. '
|
||||||
.'Thumbnails are now disabled. Please reload the page.'
|
. 'Thumbnails are now disabled. Please reload the page.'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,8 @@ public function __construct($conf)
|
||||||
*/
|
*/
|
||||||
public function get($url)
|
public function get($url)
|
||||||
{
|
{
|
||||||
if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
|
if (
|
||||||
|
$this->conf->get('thumbnails.mode') === self::MODE_COMMON
|
||||||
&& ! $this->isCommonMediaOrImage($url)
|
&& ! $this->isCommonMediaOrImage($url)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a list of available timezone continents and cities.
|
* Generates a list of available timezone continents and cities.
|
||||||
*
|
*
|
||||||
|
@ -43,7 +44,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
|
||||||
// Try to split the provided timezone
|
// Try to split the provided timezone
|
||||||
$spos = strpos($preselectedTimezone, '/');
|
$spos = strpos($preselectedTimezone, '/');
|
||||||
$pcontinent = substr($preselectedTimezone, 0, $spos);
|
$pcontinent = substr($preselectedTimezone, 0, $spos);
|
||||||
$pcity = substr($preselectedTimezone, $spos+1);
|
$pcity = substr($preselectedTimezone, $spos + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
$continents = [];
|
$continents = [];
|
||||||
|
@ -60,7 +61,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
|
||||||
}
|
}
|
||||||
|
|
||||||
$continent = substr($tz, 0, $spos);
|
$continent = substr($tz, 0, $spos);
|
||||||
$city = substr($tz, $spos+1);
|
$city = substr($tz, $spos + 1);
|
||||||
$cities[] = ['continent' => $continent, 'city' => $city];
|
$cities[] = ['continent' => $continent, 'city' => $city];
|
||||||
$continents[$continent] = true;
|
$continents[$continent] = true;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
|
||||||
function isTimeZoneValid($continent, $city)
|
function isTimeZoneValid($continent, $city)
|
||||||
{
|
{
|
||||||
return in_array(
|
return in_array(
|
||||||
$continent.'/'.$city,
|
$continent . '/' . $city,
|
||||||
timezone_identifiers_list()
|
timezone_identifiers_list()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shaarli utilities
|
* Shaarli utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs a message to a text file
|
* Format log using provided data.
|
||||||
*
|
*
|
||||||
* The log format is compatible with fail2ban.
|
* @param string $message the message to log
|
||||||
|
* @param string|null $clientIp the client's remote IPv4/IPv6 address
|
||||||
*
|
*
|
||||||
* @param string $logFile where to write the logs
|
* @return string Formatted message to log
|
||||||
* @param string $clientIp the client's remote IPv4/IPv6 address
|
|
||||||
* @param string $message the message to log
|
|
||||||
*/
|
*/
|
||||||
function logm($logFile, $clientIp, $message)
|
function format_log(string $message, string $clientIp = null): string
|
||||||
{
|
{
|
||||||
file_put_contents(
|
$out = $message;
|
||||||
$logFile,
|
|
||||||
date('Y/m/d H:i:s').' - '.$clientIp.' - '.strval($message).PHP_EOL,
|
if (!empty($clientIp)) {
|
||||||
FILE_APPEND
|
// Note: we keep the first dash to avoid breaking fail2ban configs
|
||||||
);
|
$out = '- ' . $clientIp . ' - ' . $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,7 +103,7 @@ function escape($input)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_array($input)) {
|
if (is_array($input)) {
|
||||||
$out = array();
|
$out = [];
|
||||||
foreach ($input as $key => $value) {
|
foreach ($input as $key => $value) {
|
||||||
$out[escape($key)] = escape($value);
|
$out[escape($key)] = escape($value);
|
||||||
}
|
}
|
||||||
|
@ -161,7 +164,7 @@ function checkDateFormat($format, $string)
|
||||||
*
|
*
|
||||||
* @return string $referer - final referer.
|
* @return string $referer - final referer.
|
||||||
*/
|
*/
|
||||||
function generateLocation($referer, $host, $loopTerms = array())
|
function generateLocation($referer, $host, $loopTerms = [])
|
||||||
{
|
{
|
||||||
$finalReferer = './?';
|
$finalReferer = './?';
|
||||||
|
|
||||||
|
@ -194,7 +197,7 @@ function generateLocation($referer, $host, $loopTerms = array())
|
||||||
function autoLocale($headerLocale)
|
function autoLocale($headerLocale)
|
||||||
{
|
{
|
||||||
// Default if browser does not send HTTP_ACCEPT_LANGUAGE
|
// Default if browser does not send HTTP_ACCEPT_LANGUAGE
|
||||||
$locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8');
|
$locales = ['en_US', 'en_US.utf8', 'en_US.UTF-8'];
|
||||||
if (! empty($headerLocale)) {
|
if (! empty($headerLocale)) {
|
||||||
if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) {
|
if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) {
|
||||||
$attempts = [];
|
$attempts = [];
|
||||||
|
@ -324,6 +327,23 @@ function format_date($date, $time = true, $intl = true)
|
||||||
return $formatter->format($date);
|
return $formatter->format($date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the date month according to the locale.
|
||||||
|
*
|
||||||
|
* @param DateTimeInterface $date to format.
|
||||||
|
*
|
||||||
|
* @return bool|string Formatted date, or false if the input is invalid.
|
||||||
|
*/
|
||||||
|
function format_month(DateTimeInterface $date)
|
||||||
|
{
|
||||||
|
if (! $date instanceof DateTimeInterface) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strftime('%B', $date->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the input is an integer, no matter its real type.
|
* Check if the input is an integer, no matter its real type.
|
||||||
*
|
*
|
||||||
|
@ -357,13 +377,15 @@ function return_bytes($val)
|
||||||
return $val;
|
return $val;
|
||||||
}
|
}
|
||||||
$val = trim($val);
|
$val = trim($val);
|
||||||
$last = strtolower($val[strlen($val)-1]);
|
$last = strtolower($val[strlen($val) - 1]);
|
||||||
$val = intval(substr($val, 0, -1));
|
$val = intval(substr($val, 0, -1));
|
||||||
switch ($last) {
|
switch ($last) {
|
||||||
case 'g':
|
case 'g':
|
||||||
$val *= 1024;
|
$val *= 1024;
|
||||||
|
// do no break in order 1024^2 for each unit
|
||||||
case 'm':
|
case 'm':
|
||||||
$val *= 1024;
|
$val *= 1024;
|
||||||
|
// do no break in order 1024^2 for each unit
|
||||||
case 'k':
|
case 'k':
|
||||||
$val *= 1024;
|
$val *= 1024;
|
||||||
}
|
}
|
||||||
|
@ -452,14 +474,28 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
|
||||||
* Wrapper function for translation which match the API
|
* Wrapper function for translation which match the API
|
||||||
* of gettext()/_() and ngettext().
|
* of gettext()/_() and ngettext().
|
||||||
*
|
*
|
||||||
* @param string $text Text to translate.
|
* @param string $text Text to translate.
|
||||||
* @param string $nText The plural message ID.
|
* @param string $nText The plural message ID.
|
||||||
* @param int $nb The number of items for plural forms.
|
* @param int $nb The number of items for plural forms.
|
||||||
* @param string $domain The domain where the translation is stored (default: shaarli).
|
* @param string $domain The domain where the translation is stored (default: shaarli).
|
||||||
|
* @param array $variables Associative array of variables to replace in translated text.
|
||||||
|
* @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
|
||||||
*
|
*
|
||||||
* @return string Text translated.
|
* @return string Text translated.
|
||||||
*/
|
*/
|
||||||
function t($text, $nText = '', $nb = 1, $domain = 'shaarli')
|
function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
|
||||||
{
|
{
|
||||||
return dn__($domain, $text, $nText, $nb);
|
$postFunction = $fixCase ? 'ucfirst' : function ($input) {
|
||||||
|
return $input;
|
||||||
|
};
|
||||||
|
|
||||||
|
return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an exception into a printable stack trace string.
|
||||||
|
*/
|
||||||
|
function exception2text(Throwable $e): string
|
||||||
|
{
|
||||||
|
return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Api;
|
namespace Shaarli\Api;
|
||||||
|
|
||||||
use malkusch\lock\mutex\FlockMutex;
|
use malkusch\lock\mutex\FlockMutex;
|
||||||
|
@ -108,7 +109,8 @@ protected function checkRequest($request)
|
||||||
*/
|
*/
|
||||||
protected function checkToken($request)
|
protected function checkToken($request)
|
||||||
{
|
{
|
||||||
if (!$request->hasHeader('Authorization')
|
if (
|
||||||
|
!$request->hasHeader('Authorization')
|
||||||
&& !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
|
&& !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
|
||||||
) {
|
) {
|
||||||
throw new ApiAuthorizationException('JWT token not provided');
|
throw new ApiAuthorizationException('JWT token not provided');
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Api;
|
namespace Shaarli\Api;
|
||||||
|
|
||||||
use Shaarli\Api\Exceptions\ApiAuthorizationException;
|
use Shaarli\Api\Exceptions\ApiAuthorizationException;
|
||||||
|
@ -27,7 +28,7 @@ public static function validateJwtToken($token, $secret)
|
||||||
throw new ApiAuthorizationException('Malformed JWT token');
|
throw new ApiAuthorizationException('Malformed JWT token');
|
||||||
}
|
}
|
||||||
|
|
||||||
$genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true));
|
$genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] . '.' . $parts[1], $secret, true));
|
||||||
if ($parts[2] != $genSign) {
|
if ($parts[2] != $genSign) {
|
||||||
throw new ApiAuthorizationException('Invalid JWT signature');
|
throw new ApiAuthorizationException('Invalid JWT signature');
|
||||||
}
|
}
|
||||||
|
@ -42,7 +43,8 @@ public static function validateJwtToken($token, $secret)
|
||||||
throw new ApiAuthorizationException('Invalid JWT payload');
|
throw new ApiAuthorizationException('Invalid JWT payload');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($payload->iat)
|
if (
|
||||||
|
empty($payload->iat)
|
||||||
|| $payload->iat > time()
|
|| $payload->iat > time()
|
||||||
|| time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
|
|| time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
|
||||||
) {
|
) {
|
||||||
|
@ -89,13 +91,17 @@ public static function formatLink($bookmark, $indexUrl)
|
||||||
* If no URL is provided, it will generate a local note URL.
|
* If no URL is provided, it will generate a local note URL.
|
||||||
* If no title is provided, it will use the URL as title.
|
* If no title is provided, it will use the URL as title.
|
||||||
*
|
*
|
||||||
* @param array|null $input Request Link.
|
* @param array|null $input Request Link.
|
||||||
* @param bool $defaultPrivate Setting defined if a bookmark is private by default.
|
* @param bool $defaultPrivate Setting defined if a bookmark is private by default.
|
||||||
|
* @param string $tagsSeparator Tags separator loaded from the config file.
|
||||||
*
|
*
|
||||||
* @return Bookmark instance.
|
* @return Bookmark instance.
|
||||||
*/
|
*/
|
||||||
public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark
|
public static function buildBookmarkFromRequest(
|
||||||
{
|
?array $input,
|
||||||
|
bool $defaultPrivate,
|
||||||
|
string $tagsSeparator
|
||||||
|
): Bookmark {
|
||||||
$bookmark = new Bookmark();
|
$bookmark = new Bookmark();
|
||||||
$url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
|
$url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
|
||||||
if (isset($input['private'])) {
|
if (isset($input['private'])) {
|
||||||
|
@ -107,6 +113,15 @@ public static function buildBookmarkFromRequest(?array $input, bool $defaultPriv
|
||||||
$bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
|
$bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
|
||||||
$bookmark->setUrl($url);
|
$bookmark->setUrl($url);
|
||||||
$bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
|
$bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
|
||||||
|
|
||||||
|
// Be permissive with provided tags format
|
||||||
|
if (is_string($input['tags'] ?? null)) {
|
||||||
|
$input['tags'] = tags_str2array($input['tags'], $tagsSeparator);
|
||||||
|
}
|
||||||
|
if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) {
|
||||||
|
$input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
$bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
|
$bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
|
||||||
$bookmark->setPrivate($private);
|
$bookmark->setPrivate($private);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Api\Controllers;
|
namespace Shaarli\Api\Controllers;
|
||||||
|
|
||||||
use Shaarli\Api\Exceptions\ApiBadParametersException;
|
use Shaarli\Api\Exceptions\ApiBadParametersException;
|
||||||
|
|
|
@ -29,13 +29,13 @@ public function getInfo($request, $response)
|
||||||
$info = [
|
$info = [
|
||||||
'global_counter' => $this->bookmarkService->count(),
|
'global_counter' => $this->bookmarkService->count(),
|
||||||
'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
|
'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
|
||||||
'settings' => array(
|
'settings' => [
|
||||||
'title' => $this->conf->get('general.title', 'Shaarli'),
|
'title' => $this->conf->get('general.title', 'Shaarli'),
|
||||||
'header_link' => $this->conf->get('general.header_link', '?'),
|
'header_link' => $this->conf->get('general.header_link', '?'),
|
||||||
'timezone' => $this->conf->get('general.timezone', 'UTC'),
|
'timezone' => $this->conf->get('general.timezone', 'UTC'),
|
||||||
'enabled_plugins' => $this->conf->get('general.enabled_plugins', []),
|
'enabled_plugins' => $this->conf->get('general.enabled_plugins', []),
|
||||||
'default_private_links' => $this->conf->get('privacy.default_private_links', false),
|
'default_private_links' => $this->conf->get('privacy.default_private_links', false),
|
||||||
),
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
return $response->withJson($info, 200, $this->jsonStyle);
|
return $response->withJson($info, 200, $this->jsonStyle);
|
||||||
|
|
|
@ -117,9 +117,14 @@ public function getLink($request, $response, $args)
|
||||||
public function postLink($request, $response)
|
public function postLink($request, $response)
|
||||||
{
|
{
|
||||||
$data = (array) ($request->getParsedBody() ?? []);
|
$data = (array) ($request->getParsedBody() ?? []);
|
||||||
$bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
|
$bookmark = ApiUtils::buildBookmarkFromRequest(
|
||||||
|
$data,
|
||||||
|
$this->conf->get('privacy.default_private_links'),
|
||||||
|
$this->conf->get('general.tags_separator', ' ')
|
||||||
|
);
|
||||||
// duplicate by URL, return 409 Conflict
|
// duplicate by URL, return 409 Conflict
|
||||||
if (! empty($bookmark->getUrl())
|
if (
|
||||||
|
! empty($bookmark->getUrl())
|
||||||
&& ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
|
&& ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
|
||||||
) {
|
) {
|
||||||
return $response->withJson(
|
return $response->withJson(
|
||||||
|
@ -131,7 +136,7 @@ public function postLink($request, $response)
|
||||||
|
|
||||||
$this->bookmarkService->add($bookmark);
|
$this->bookmarkService->add($bookmark);
|
||||||
$out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
|
$out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
|
||||||
$redirect = $this->ci->router->relativePathFor('getLink', ['id' => $bookmark->getId()]);
|
$redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]);
|
||||||
return $response->withAddedHeader('Location', $redirect)
|
return $response->withAddedHeader('Location', $redirect)
|
||||||
->withJson($out, 201, $this->jsonStyle);
|
->withJson($out, 201, $this->jsonStyle);
|
||||||
}
|
}
|
||||||
|
@ -157,9 +162,14 @@ public function putLink($request, $response, $args)
|
||||||
$index = index_url($this->ci['environment']);
|
$index = index_url($this->ci['environment']);
|
||||||
$data = $request->getParsedBody();
|
$data = $request->getParsedBody();
|
||||||
|
|
||||||
$requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
|
$requestBookmark = ApiUtils::buildBookmarkFromRequest(
|
||||||
|
$data,
|
||||||
|
$this->conf->get('privacy.default_private_links'),
|
||||||
|
$this->conf->get('general.tags_separator', ' ')
|
||||||
|
);
|
||||||
// duplicate URL on a different link, return 409 Conflict
|
// duplicate URL on a different link, return 409 Conflict
|
||||||
if (! empty($requestBookmark->getUrl())
|
if (
|
||||||
|
! empty($requestBookmark->getUrl())
|
||||||
&& ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
|
&& ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
|
||||||
&& $dup->getId() != $id
|
&& $dup->getId() != $id
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -28,7 +28,7 @@ public function getApiResponse()
|
||||||
*/
|
*/
|
||||||
public function setMessage($message)
|
public function setMessage($message)
|
||||||
{
|
{
|
||||||
$original = $this->debug === true ? ': '. $this->getMessage() : '';
|
$original = $this->debug === true ? ': ' . $this->getMessage() : '';
|
||||||
$this->message = $message . $original;
|
$this->message = $message . $original;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ protected function getApiResponseBody()
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
'message' => $this->getMessage(),
|
'message' => $this->getMessage(),
|
||||||
'stacktrace' => get_class($this) .': '. $this->getTraceAsString()
|
'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString()
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
class Bookmark
|
class Bookmark
|
||||||
{
|
{
|
||||||
/** @var string Date format used in string (former ID format) */
|
/** @var string Date format used in string (former ID format) */
|
||||||
const LINK_DATE_FORMAT = 'Ymd_His';
|
public const LINK_DATE_FORMAT = 'Ymd_His';
|
||||||
|
|
||||||
/** @var int Bookmark ID */
|
/** @var int Bookmark ID */
|
||||||
protected $id;
|
protected $id;
|
||||||
|
@ -60,11 +60,13 @@ class Bookmark
|
||||||
/**
|
/**
|
||||||
* Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
|
* Initialize a link from array data. Especially useful to create a Bookmark from former link storage format.
|
||||||
*
|
*
|
||||||
* @param array $data
|
* @param array $data
|
||||||
|
* @param string $tagsSeparator Tags separator loaded from the config file.
|
||||||
|
* This is a context data, and it should *never* be stored in the Bookmark object.
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function fromArray(array $data): Bookmark
|
public function fromArray(array $data, string $tagsSeparator = ' '): Bookmark
|
||||||
{
|
{
|
||||||
$this->id = $data['id'] ?? null;
|
$this->id = $data['id'] ?? null;
|
||||||
$this->shortUrl = $data['shorturl'] ?? null;
|
$this->shortUrl = $data['shorturl'] ?? null;
|
||||||
|
@ -77,7 +79,7 @@ public function fromArray(array $data): Bookmark
|
||||||
if (is_array($data['tags'])) {
|
if (is_array($data['tags'])) {
|
||||||
$this->tags = $data['tags'];
|
$this->tags = $data['tags'];
|
||||||
} else {
|
} else {
|
||||||
$this->tags = preg_split('/\s+/', $data['tags'] ?? '', -1, PREG_SPLIT_NO_EMPTY);
|
$this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
|
||||||
}
|
}
|
||||||
if (! empty($data['updated'])) {
|
if (! empty($data['updated'])) {
|
||||||
$this->updated = $data['updated'];
|
$this->updated = $data['updated'];
|
||||||
|
@ -104,7 +106,8 @@ public function fromArray(array $data): Bookmark
|
||||||
*/
|
*/
|
||||||
public function validate(): void
|
public function validate(): void
|
||||||
{
|
{
|
||||||
if ($this->id === null
|
if (
|
||||||
|
$this->id === null
|
||||||
|| ! is_int($this->id)
|
|| ! is_int($this->id)
|
||||||
|| empty($this->shortUrl)
|
|| empty($this->shortUrl)
|
||||||
|| empty($this->created)
|
|| empty($this->created)
|
||||||
|
@ -112,7 +115,7 @@ public function validate(): void
|
||||||
throw new InvalidBookmarkException($this);
|
throw new InvalidBookmarkException($this);
|
||||||
}
|
}
|
||||||
if (empty($this->url)) {
|
if (empty($this->url)) {
|
||||||
$this->url = '/shaare/'. $this->shortUrl;
|
$this->url = '/shaare/' . $this->shortUrl;
|
||||||
}
|
}
|
||||||
if (empty($this->title)) {
|
if (empty($this->title)) {
|
||||||
$this->title = $this->url;
|
$this->title = $this->url;
|
||||||
|
@ -348,7 +351,12 @@ public function getTags(): array
|
||||||
*/
|
*/
|
||||||
public function setTags(?array $tags): Bookmark
|
public function setTags(?array $tags): Bookmark
|
||||||
{
|
{
|
||||||
$this->setTagsString(implode(' ', $tags ?? []));
|
$this->tags = array_map(
|
||||||
|
function (string $tag): string {
|
||||||
|
return $tag[0] === '-' ? substr($tag, 1) : $tag;
|
||||||
|
},
|
||||||
|
tags_filter($tags, ' ')
|
||||||
|
);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -377,6 +385,24 @@ public function setThumbnail($thumbnail): Bookmark
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if:
|
||||||
|
* - the bookmark's thumbnail is not already set to false (= not found)
|
||||||
|
* - it's not a note
|
||||||
|
* - it's an HTTP(S) link
|
||||||
|
* - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
|
||||||
|
*
|
||||||
|
* @return bool True if the bookmark's thumbnail needs to be retrieved.
|
||||||
|
*/
|
||||||
|
public function shouldUpdateThumbnail(): bool
|
||||||
|
{
|
||||||
|
return $this->thumbnail !== false
|
||||||
|
&& !$this->isNote()
|
||||||
|
&& startsWith(strtolower($this->url), 'http')
|
||||||
|
&& (null === $this->thumbnail || !is_file($this->thumbnail))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Sticky.
|
* Get the Sticky.
|
||||||
*
|
*
|
||||||
|
@ -402,11 +428,13 @@ public function setSticky(?bool $sticky): Bookmark
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return string Bookmark's tags as a string, separated by a space
|
* @param string $separator Tags separator loaded from the config file.
|
||||||
|
*
|
||||||
|
* @return string Bookmark's tags as a string, separated by a separator
|
||||||
*/
|
*/
|
||||||
public function getTagsString(): string
|
public function getTagsString(string $separator = ' '): string
|
||||||
{
|
{
|
||||||
return implode(' ', $this->getTags());
|
return tags_array2str($this->getTags(), $separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -426,19 +454,13 @@ public function isNote(): bool
|
||||||
* - trailing dash in tags will be removed
|
* - trailing dash in tags will be removed
|
||||||
*
|
*
|
||||||
* @param string|null $tags
|
* @param string|null $tags
|
||||||
|
* @param string $separator Tags separator loaded from the config file.
|
||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setTagsString(?string $tags): Bookmark
|
public function setTagsString(?string $tags, string $separator = ' '): Bookmark
|
||||||
{
|
{
|
||||||
// Remove first '-' char in tags.
|
$this->setTags(tags_str2array($tags, $separator));
|
||||||
$tags = preg_replace('/(^| )\-/', '$1', $tags ?? '');
|
|
||||||
// Explode all tags separted by spaces or commas
|
|
||||||
$tags = preg_split('/[\s,]+/', $tags);
|
|
||||||
// Remove eventual empty values
|
|
||||||
$tags = array_values(array_filter($tags));
|
|
||||||
|
|
||||||
$this->tags = $tags;
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -489,7 +511,7 @@ public function getAdditionalContentEntry(string $key, $default = null)
|
||||||
*/
|
*/
|
||||||
public function renameTag(string $fromTag, string $toTag): void
|
public function renameTag(string $fromTag, string $toTag): void
|
||||||
{
|
{
|
||||||
if (($pos = array_search($fromTag, $this->tags)) !== false) {
|
if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
|
||||||
$this->tags[$pos] = trim($toTag);
|
$this->tags[$pos] = trim($toTag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -501,7 +523,7 @@ public function renameTag(string $fromTag, string $toTag): void
|
||||||
*/
|
*/
|
||||||
public function deleteTag(string $tag): void
|
public function deleteTag(string $tag): void
|
||||||
{
|
{
|
||||||
if (($pos = array_search($tag, $this->tags)) !== false) {
|
if (($pos = array_search($tag, $this->tags ?? [])) !== false) {
|
||||||
unset($this->tags[$pos]);
|
unset($this->tags[$pos]);
|
||||||
$this->tags = array_values($this->tags);
|
$this->tags = array_values($this->tags);
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,8 @@ public function count()
|
||||||
*/
|
*/
|
||||||
public function offsetSet($offset, $value)
|
public function offsetSet($offset, $value)
|
||||||
{
|
{
|
||||||
if (! $value instanceof Bookmark
|
if (
|
||||||
|
! $value instanceof Bookmark
|
||||||
|| $value->getId() === null || empty($value->getUrl())
|
|| $value->getId() === null || empty($value->getUrl())
|
||||||
|| ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
|
|| ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
|
||||||
|| $offset !== null && $offset !== $value->getId()
|
|| $offset !== null && $offset !== $value->getId()
|
||||||
|
@ -222,7 +223,8 @@ public function getNextId(): int
|
||||||
*/
|
*/
|
||||||
public function getByUrl(string $url): ?Bookmark
|
public function getByUrl(string $url): ?Bookmark
|
||||||
{
|
{
|
||||||
if (! empty($url)
|
if (
|
||||||
|
! empty($url)
|
||||||
&& isset($this->urls[$url])
|
&& isset($this->urls[$url])
|
||||||
&& isset($this->bookmarks[$this->urls[$url]])
|
&& isset($this->bookmarks[$this->urls[$url]])
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -69,7 +69,7 @@ public function __construct(ConfigManager $conf, History $history, Mutex $mutex,
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$this->bookmarks = $this->bookmarksIO->read();
|
$this->bookmarks = $this->bookmarksIO->read();
|
||||||
} catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
|
} catch (EmptyDataStoreException | DatastoreNotInitializedException $e) {
|
||||||
$this->bookmarks = new BookmarkArray();
|
$this->bookmarks = new BookmarkArray();
|
||||||
|
|
||||||
if ($this->isLoggedIn) {
|
if ($this->isLoggedIn) {
|
||||||
|
@ -85,25 +85,29 @@ public function __construct(ConfigManager $conf, History $history, Mutex $mutex,
|
||||||
if (! $this->bookmarks instanceof BookmarkArray) {
|
if (! $this->bookmarks instanceof BookmarkArray) {
|
||||||
$this->migrate();
|
$this->migrate();
|
||||||
exit(
|
exit(
|
||||||
'Your data store has been migrated, please reload the page.'. PHP_EOL .
|
'Your data store has been migrated, please reload the page.' . PHP_EOL .
|
||||||
'If this message keeps showing up, please delete data/updates.txt file.'
|
'If this message keeps showing up, please delete data/updates.txt file.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
|
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function findByHash(string $hash): Bookmark
|
public function findByHash(string $hash, string $privateKey = null): Bookmark
|
||||||
{
|
{
|
||||||
$bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
|
$bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
|
||||||
// PHP 7.3 introduced array_key_first() to avoid this hack
|
// PHP 7.3 introduced array_key_first() to avoid this hack
|
||||||
$first = reset($bookmark);
|
$first = reset($bookmark);
|
||||||
if (! $this->isLoggedIn && $first->isPrivate()) {
|
if (
|
||||||
throw new Exception('Not authorized');
|
!$this->isLoggedIn
|
||||||
|
&& $first->isPrivate()
|
||||||
|
&& (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
|
||||||
|
) {
|
||||||
|
throw new BookmarkNotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $first;
|
return $first;
|
||||||
|
@ -162,7 +166,8 @@ public function get(int $id, string $visibility = null): Bookmark
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmark = $this->bookmarks[$id];
|
$bookmark = $this->bookmarks[$id];
|
||||||
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
if (
|
||||||
|
($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
||||||
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
||||||
) {
|
) {
|
||||||
throw new Exception('Unauthorized');
|
throw new Exception('Unauthorized');
|
||||||
|
@ -262,7 +267,8 @@ public function exists(int $id, string $visibility = null): bool
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmark = $this->bookmarks[$id];
|
$bookmark = $this->bookmarks[$id];
|
||||||
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
if (
|
||||||
|
($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
||||||
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -304,7 +310,8 @@ public function bookmarksCountPerTag(array $filteringTags = [], string $visibili
|
||||||
$caseMapping = [];
|
$caseMapping = [];
|
||||||
foreach ($bookmarks as $bookmark) {
|
foreach ($bookmarks as $bookmark) {
|
||||||
foreach ($bookmark->getTags() as $tag) {
|
foreach ($bookmark->getTags() as $tag) {
|
||||||
if (empty($tag)
|
if (
|
||||||
|
empty($tag)
|
||||||
|| (! $this->isLoggedIn && startsWith($tag, '.'))
|
|| (! $this->isLoggedIn && startsWith($tag, '.'))
|
||||||
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG
|
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG
|
||||||
|| in_array($tag, $filteringTags, true)
|
|| in_array($tag, $filteringTags, true)
|
||||||
|
@ -340,26 +347,42 @@ public function bookmarksCountPerTag(array $filteringTags = [], string $visibili
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function days(): array
|
public function findByDate(
|
||||||
{
|
\DateTimeInterface $from,
|
||||||
$bookmarkDays = [];
|
\DateTimeInterface $to,
|
||||||
foreach ($this->search() as $bookmark) {
|
?\DateTimeInterface &$previous,
|
||||||
$bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
|
?\DateTimeInterface &$next
|
||||||
}
|
): array {
|
||||||
$bookmarkDays = array_keys($bookmarkDays);
|
$out = [];
|
||||||
sort($bookmarkDays);
|
$previous = null;
|
||||||
|
$next = null;
|
||||||
|
|
||||||
return array_map('strval', $bookmarkDays);
|
foreach ($this->search([], null, false, false, true) as $bookmark) {
|
||||||
|
if ($to < $bookmark->getCreated()) {
|
||||||
|
$next = $bookmark->getCreated();
|
||||||
|
} elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
|
||||||
|
$out[] = $bookmark;
|
||||||
|
} else {
|
||||||
|
if ($previous !== null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$previous = $bookmark->getCreated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
public function filterDay(string $request)
|
public function getLatest(): ?Bookmark
|
||||||
{
|
{
|
||||||
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
|
foreach ($this->search([], null, false, false, true) as $bookmark) {
|
||||||
|
return $bookmark;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -386,14 +409,14 @@ protected function migrate(): void
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
$updater = new LegacyUpdater(
|
$updater = new LegacyUpdater(
|
||||||
UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
|
UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')),
|
||||||
$bookmarkDb,
|
$bookmarkDb,
|
||||||
$this->conf,
|
$this->conf,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
$newUpdates = $updater->update();
|
$newUpdates = $updater->update();
|
||||||
if (! empty($newUpdates)) {
|
if (! empty($newUpdates)) {
|
||||||
UpdaterUtils::write_updates_file(
|
UpdaterUtils::writeUpdatesFile(
|
||||||
$this->conf->get('resource.updates'),
|
$this->conf->get('resource.updates'),
|
||||||
$updater->getDoneUpdates()
|
$updater->getDoneUpdates()
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||||
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class LinkFilter.
|
* Class LinkFilter.
|
||||||
|
@ -58,12 +59,16 @@ class BookmarkFilter
|
||||||
*/
|
*/
|
||||||
private $bookmarks;
|
private $bookmarks;
|
||||||
|
|
||||||
|
/** @var ConfigManager */
|
||||||
|
protected $conf;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Bookmark[] $bookmarks initialization.
|
* @param Bookmark[] $bookmarks initialization.
|
||||||
*/
|
*/
|
||||||
public function __construct($bookmarks)
|
public function __construct($bookmarks, ConfigManager $conf)
|
||||||
{
|
{
|
||||||
$this->bookmarks = $bookmarks;
|
$this->bookmarks = $bookmarks;
|
||||||
|
$this->conf = $conf;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,10 +112,14 @@ public function filter(
|
||||||
$filtered = $this->bookmarks;
|
$filtered = $this->bookmarks;
|
||||||
}
|
}
|
||||||
if (!empty($request[0])) {
|
if (!empty($request[0])) {
|
||||||
$filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
|
$filtered = (new BookmarkFilter($filtered, $this->conf))
|
||||||
|
->filterTags($request[0], $casesensitive, $visibility)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
if (!empty($request[1])) {
|
if (!empty($request[1])) {
|
||||||
$filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
|
$filtered = (new BookmarkFilter($filtered, $this->conf))
|
||||||
|
->filterFulltext($request[1], $visibility)
|
||||||
|
;
|
||||||
}
|
}
|
||||||
return $filtered;
|
return $filtered;
|
||||||
case self::$FILTER_TEXT:
|
case self::$FILTER_TEXT:
|
||||||
|
@ -141,7 +150,7 @@ private function noFilter(string $visibility = 'all')
|
||||||
return $this->bookmarks;
|
return $this->bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = array();
|
$out = [];
|
||||||
foreach ($this->bookmarks as $key => $value) {
|
foreach ($this->bookmarks as $key => $value) {
|
||||||
if ($value->isPrivate() && $visibility === 'private') {
|
if ($value->isPrivate() && $visibility === 'private') {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
|
@ -280,8 +289,9 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
|
||||||
*
|
*
|
||||||
* @return string generated regex fragment
|
* @return string generated regex fragment
|
||||||
*/
|
*/
|
||||||
private static function tag2regex(string $tag): string
|
protected function tag2regex(string $tag): string
|
||||||
{
|
{
|
||||||
|
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||||
$len = strlen($tag);
|
$len = strlen($tag);
|
||||||
if (!$len || $tag === "-" || $tag === "*") {
|
if (!$len || $tag === "-" || $tag === "*") {
|
||||||
// nothing to search, return empty regex
|
// nothing to search, return empty regex
|
||||||
|
@ -295,12 +305,13 @@ private static function tag2regex(string $tag): string
|
||||||
$i = 0; // start at first character
|
$i = 0; // start at first character
|
||||||
$regex = '(?='; // use positive lookahead
|
$regex = '(?='; // use positive lookahead
|
||||||
}
|
}
|
||||||
$regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
|
// before tag may only be the separator or the beginning
|
||||||
|
$regex .= '.*(?:^|' . $tagsSeparator . ')';
|
||||||
// iterate over string, separating it into placeholder and content
|
// iterate over string, separating it into placeholder and content
|
||||||
for (; $i < $len; $i++) {
|
for (; $i < $len; $i++) {
|
||||||
if ($tag[$i] === '*') {
|
if ($tag[$i] === '*') {
|
||||||
// placeholder found
|
// placeholder found
|
||||||
$regex .= '[^ ]*?';
|
$regex .= '[^' . $tagsSeparator . ']*?';
|
||||||
} else {
|
} else {
|
||||||
// regular characters
|
// regular characters
|
||||||
$offset = strpos($tag, '*', $i);
|
$offset = strpos($tag, '*', $i);
|
||||||
|
@ -316,7 +327,8 @@ private static function tag2regex(string $tag): string
|
||||||
$i = $offset;
|
$i = $offset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$regex .= '(?:$| ))'; // after the tag may only be a space or the end
|
// after the tag may only be the separator or the end
|
||||||
|
$regex .= '(?:$|' . $tagsSeparator . '))';
|
||||||
return $regex;
|
return $regex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -334,14 +346,15 @@ private static function tag2regex(string $tag): string
|
||||||
*/
|
*/
|
||||||
public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
|
public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
|
||||||
{
|
{
|
||||||
|
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||||
// get single tags (we may get passed an array, even though the docs say different)
|
// get single tags (we may get passed an array, even though the docs say different)
|
||||||
$inputTags = $tags;
|
$inputTags = $tags;
|
||||||
if (!is_array($tags)) {
|
if (!is_array($tags)) {
|
||||||
// we got an input string, split tags
|
// we got an input string, split tags
|
||||||
$inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
|
$inputTags = tags_str2array($inputTags, $tagsSeparator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!count($inputTags)) {
|
if (count($inputTags) === 0) {
|
||||||
// no input tags
|
// no input tags
|
||||||
return $this->noFilter($visibility);
|
return $this->noFilter($visibility);
|
||||||
}
|
}
|
||||||
|
@ -358,7 +371,7 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
||||||
}
|
}
|
||||||
|
|
||||||
// build regex from all tags
|
// build regex from all tags
|
||||||
$re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
|
$re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
|
||||||
if (!$casesensitive) {
|
if (!$casesensitive) {
|
||||||
// make regex case insensitive
|
// make regex case insensitive
|
||||||
$re .= 'i';
|
$re .= 'i';
|
||||||
|
@ -378,10 +391,11 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$search = $link->getTagsString(); // build search string, start with tags of current link
|
// build search string, start with tags of current link
|
||||||
|
$search = $link->getTagsString($tagsSeparator);
|
||||||
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
|
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
|
||||||
// description given and at least one possible tag found
|
// description given and at least one possible tag found
|
||||||
$descTags = array();
|
$descTags = [];
|
||||||
// find all tags in the form of #tag in the description
|
// find all tags in the form of #tag in the description
|
||||||
preg_match_all(
|
preg_match_all(
|
||||||
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
|
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
|
||||||
|
@ -390,9 +404,9 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
|
||||||
);
|
);
|
||||||
if (count($descTags[1])) {
|
if (count($descTags[1])) {
|
||||||
// there were some tags in the description, add them to the search string
|
// there were some tags in the description, add them to the search string
|
||||||
$search .= ' ' . implode(' ', $descTags[1]);
|
$search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
// match regular expression with search string
|
// match regular expression with search string
|
||||||
if (!preg_match($re, $search)) {
|
if (!preg_match($re, $search)) {
|
||||||
// this entry does _not_ match our regex
|
// this entry does _not_ match our regex
|
||||||
|
@ -422,7 +436,7 @@ public function filterUntagged(string $visibility)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty(trim($link->getTagsString()))) {
|
if (empty($link->getTags())) {
|
||||||
$filtered[$key] = $link;
|
$filtered[$key] = $link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -537,10 +551,11 @@ protected function postProcessFoundPositions(array $fieldLengths, array $foundPo
|
||||||
*/
|
*/
|
||||||
protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
|
protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
|
||||||
{
|
{
|
||||||
$content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
|
$tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
|
||||||
$content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
|
$content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\';
|
||||||
$content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
|
$content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\';
|
||||||
$content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
|
$content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\';
|
||||||
|
$content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\';
|
||||||
|
|
||||||
$lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
|
$lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
|
||||||
$nextField = $lengths['title']['end'] + 1;
|
$nextField = $lengths['title']['end'] + 1;
|
||||||
|
@ -548,7 +563,7 @@ protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths):
|
||||||
$nextField = $lengths['description']['end'] + 1;
|
$nextField = $lengths['description']['end'] + 1;
|
||||||
$lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
|
$lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
|
||||||
$nextField = $lengths['url']['end'] + 1;
|
$nextField = $lengths['url']['end'] + 1;
|
||||||
$lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())];
|
$lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
namespace Shaarli\Bookmark;
|
namespace Shaarli\Bookmark;
|
||||||
|
|
||||||
|
use malkusch\lock\exception\LockAcquireException;
|
||||||
use malkusch\lock\mutex\Mutex;
|
use malkusch\lock\mutex\Mutex;
|
||||||
use malkusch\lock\mutex\NoMutex;
|
use malkusch\lock\mutex\NoMutex;
|
||||||
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
|
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
|
||||||
|
@ -80,7 +81,7 @@ public function read()
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = null;
|
$content = null;
|
||||||
$this->mutex->synchronized(function () use (&$content) {
|
$this->synchronized(function () use (&$content) {
|
||||||
$content = file_get_contents($this->datastore);
|
$content = file_get_contents($this->datastore);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -112,18 +113,35 @@ public function write($links)
|
||||||
if (is_file($this->datastore) && !is_writeable($this->datastore)) {
|
if (is_file($this->datastore) && !is_writeable($this->datastore)) {
|
||||||
// The datastore exists but is not writeable
|
// The datastore exists but is not writeable
|
||||||
throw new NotWritableDataStoreException($this->datastore);
|
throw new NotWritableDataStoreException($this->datastore);
|
||||||
} else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
|
} elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
|
||||||
// The datastore does not exist and its parent directory is not writeable
|
// The datastore does not exist and its parent directory is not writeable
|
||||||
throw new NotWritableDataStoreException(dirname($this->datastore));
|
throw new NotWritableDataStoreException(dirname($this->datastore));
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = self::$phpPrefix.base64_encode(gzdeflate(serialize($links))).self::$phpSuffix;
|
$data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;
|
||||||
|
|
||||||
$this->mutex->synchronized(function () use ($data) {
|
$this->synchronized(function () use ($data) {
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$this->datastore,
|
$this->datastore,
|
||||||
$data
|
$data
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper applying mutex to provided function.
|
||||||
|
* If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex.
|
||||||
|
*
|
||||||
|
* @see https://github.com/shaarli/Shaarli/issues/1650
|
||||||
|
*
|
||||||
|
* @param callable $function
|
||||||
|
*/
|
||||||
|
protected function synchronized(callable $function): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->mutex->synchronized($function);
|
||||||
|
} catch (LockAcquireException $exception) {
|
||||||
|
$function();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
* To prevent data corruption, it does not overwrite existing bookmarks,
|
* To prevent data corruption, it does not overwrite existing bookmarks,
|
||||||
* even though there should not be any.
|
* even though there should not be any.
|
||||||
*
|
*
|
||||||
|
* We disable this because otherwise it creates indentation issues, and heredoc is not supported by PHP gettext.
|
||||||
|
* @phpcs:disable Generic.Files.LineLength.TooLong
|
||||||
|
*
|
||||||
* @package Shaarli\Bookmark
|
* @package Shaarli\Bookmark
|
||||||
*/
|
*/
|
||||||
class BookmarkInitializer
|
class BookmarkInitializer
|
||||||
|
@ -36,10 +39,10 @@ public function __construct(BookmarkServiceInterface $bookmarkService)
|
||||||
public function initialize(): void
|
public function initialize(): void
|
||||||
{
|
{
|
||||||
$bookmark = new Bookmark();
|
$bookmark = new Bookmark();
|
||||||
$bookmark->setTitle('quicksilver (loop) on Vimeo ' . t('(private bookmark with thumbnail demo)'));
|
$bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)'));
|
||||||
$bookmark->setUrl('https://vimeo.com/153493904');
|
$bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c');
|
||||||
$bookmark->setDescription(t(
|
$bookmark->setDescription(t(
|
||||||
'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
|
'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
|
||||||
|
|
||||||
Explore your new Shaarli instance by trying out controls and menus.
|
Explore your new Shaarli instance by trying out controls and menus.
|
||||||
Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
|
Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
|
||||||
|
@ -54,7 +57,7 @@ public function initialize(): void
|
||||||
$bookmark = new Bookmark();
|
$bookmark = new Bookmark();
|
||||||
$bookmark->setTitle(t('Note: Shaare descriptions'));
|
$bookmark->setTitle(t('Note: Shaare descriptions'));
|
||||||
$bookmark->setDescription(t(
|
$bookmark->setDescription(t(
|
||||||
'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
|
'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
|
||||||
This note is private, so you are the only one able to see it while logged in.
|
This note is private, so you are the only one able to see it while logged in.
|
||||||
|
|
||||||
You can use this to keep notes, post articles, code snippets, and much more.
|
You can use this to keep notes, post articles, code snippets, and much more.
|
||||||
|
@ -91,7 +94,7 @@ public function initialize(): void
|
||||||
'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
|
'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
|
||||||
);
|
);
|
||||||
$bookmark->setDescription(t(
|
$bookmark->setDescription(t(
|
||||||
'Welcome to Shaarli!
|
'Welcome to Shaarli!
|
||||||
|
|
||||||
Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
|
Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
|
||||||
You can add a description to your bookmarks, such as this one, and tag them.
|
You can add a description to your bookmarks, such as this one, and tag them.
|
||||||
|
|
|
@ -20,13 +20,14 @@ interface BookmarkServiceInterface
|
||||||
/**
|
/**
|
||||||
* Find a bookmark by hash
|
* Find a bookmark by hash
|
||||||
*
|
*
|
||||||
* @param string $hash
|
* @param string $hash Bookmark's hash
|
||||||
|
* @param string|null $privateKey Optional key used to access private links while logged out
|
||||||
*
|
*
|
||||||
* @return Bookmark
|
* @return Bookmark
|
||||||
*
|
*
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
public function findByHash(string $hash): Bookmark;
|
public function findByHash(string $hash, string $privateKey = null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param $url
|
* @param $url
|
||||||
|
@ -155,22 +156,29 @@ public function save(): void;
|
||||||
public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
|
public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of days containing articles (oldest first)
|
* Return a list of bookmark matching provided period of time.
|
||||||
|
* It also update directly previous and next date outside of given period found in the datastore.
|
||||||
*
|
*
|
||||||
* @return array containing days (in format YYYYMMDD).
|
* @param \DateTimeInterface $from Starting date.
|
||||||
|
* @param \DateTimeInterface $to Ending date.
|
||||||
|
* @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
|
||||||
|
* @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
|
||||||
|
*
|
||||||
|
* @return array List of bookmarks matching provided period of time.
|
||||||
*/
|
*/
|
||||||
public function days(): array;
|
public function findByDate(
|
||||||
|
\DateTimeInterface $from,
|
||||||
|
\DateTimeInterface $to,
|
||||||
|
?\DateTimeInterface &$previous,
|
||||||
|
?\DateTimeInterface &$next
|
||||||
|
): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the list of articles for a given day.
|
* Returns the latest bookmark by creation date.
|
||||||
*
|
*
|
||||||
* @param string $request day to filter. Format: YYYYMMDD.
|
* @return Bookmark|null Found Bookmark or null if the datastore is empty.
|
||||||
*
|
|
||||||
* @return Bookmark[] list of shaare found.
|
|
||||||
*
|
|
||||||
* @throws BookmarkNotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function filterDay(string $request);
|
public function getLatest(): ?Bookmark;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the default database after a fresh install.
|
* Creates the default database after a fresh install.
|
||||||
|
|
|
@ -67,17 +67,20 @@ function html_extract_tag($tag, $html)
|
||||||
$propertiesKey = ['property', 'name', 'itemprop'];
|
$propertiesKey = ['property', 'name', 'itemprop'];
|
||||||
$properties = implode('|', $propertiesKey);
|
$properties = implode('|', $propertiesKey);
|
||||||
// We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
|
// We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
|
||||||
$orCondition = '["\']?(?:og:)?'. $tag .'["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
|
$orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
|
||||||
// Try to retrieve OpenGraph image.
|
// Support quotes in double quoted content, and the other way around
|
||||||
$ogRegex = '#<meta[^>]+(?:'. $properties .')=(?:'. $orCondition .')[^>]*content=["\'](.*?)["\'].*?>#';
|
$content = 'content=(["\'])((?:(?!\1).)*)\1';
|
||||||
|
// Try to retrieve OpenGraph tag.
|
||||||
|
$ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
|
||||||
// If the attributes are not in the order property => content (e.g. Github)
|
// If the attributes are not in the order property => content (e.g. Github)
|
||||||
// New regex to keep this readable... more or less.
|
// New regex to keep this readable... more or less.
|
||||||
$ogRegexReverse = '#<meta[^>]+content=["\'](.*?)["\'][^>]+(?:'. $properties .')=(?:'. $orCondition .').*?>#';
|
$ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
|
||||||
|
|
||||||
if (preg_match($ogRegex, $html, $matches) > 0
|
if (
|
||||||
|
preg_match($ogRegex, $html, $matches) > 0
|
||||||
|| preg_match($ogRegexReverse, $html, $matches) > 0
|
|| preg_match($ogRegexReverse, $html, $matches) > 0
|
||||||
) {
|
) {
|
||||||
return $matches[1];
|
return $matches[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -116,7 +119,7 @@ function hashtag_autolink($description, $indexUrl = '')
|
||||||
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
||||||
*/
|
*/
|
||||||
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
||||||
$replacement = '$1<a href="'. $indexUrl .'./add-tag/$2" title="Hashtag $2">#$2</a>';
|
$replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>';
|
||||||
return preg_replace($regex, $replacement, $description);
|
return preg_replace($regex, $replacement, $description);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,12 +141,17 @@ function space2nbsp($text)
|
||||||
*
|
*
|
||||||
* @param string $description shaare's description.
|
* @param string $description shaare's description.
|
||||||
* @param string $indexUrl URL to Shaarli's index.
|
* @param string $indexUrl URL to Shaarli's index.
|
||||||
|
* @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags
|
||||||
|
*
|
||||||
* @return string formatted description.
|
* @return string formatted description.
|
||||||
*/
|
*/
|
||||||
function format_description($description, $indexUrl = '')
|
function format_description($description, $indexUrl = '', $autolink = true)
|
||||||
{
|
{
|
||||||
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl)));
|
if ($autolink) {
|
||||||
|
$description = hashtag_autolink(text2clickable($description), $indexUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nl2br(space2nbsp($description));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,3 +179,49 @@ function is_note($linkUrl)
|
||||||
{
|
{
|
||||||
return isset($linkUrl[0]) && $linkUrl[0] === '?';
|
return isset($linkUrl[0]) && $linkUrl[0] === '?';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract an array of tags from a given tag string, with provided separator.
|
||||||
|
*
|
||||||
|
* @param string|null $tags String containing a list of tags separated by $separator.
|
||||||
|
* @param string $separator Shaarli's default: ' ' (whitespace)
|
||||||
|
*
|
||||||
|
* @return array List of tags
|
||||||
|
*/
|
||||||
|
function tags_str2array(?string $tags, string $separator): array
|
||||||
|
{
|
||||||
|
// For whitespaces, we use the special \s regex character
|
||||||
|
$separator = $separator === ' ' ? '\s' : $separator;
|
||||||
|
|
||||||
|
return preg_split('/\s*' . $separator . '+\s*/', trim($tags) ?? '', -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a tag string with provided separator from a list of tags.
|
||||||
|
* Note that given array is clean up by tags_filter().
|
||||||
|
*
|
||||||
|
* @param array|null $tags List of tags
|
||||||
|
* @param string $separator
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function tags_array2str(?array $tags, string $separator): string
|
||||||
|
{
|
||||||
|
return implode($separator, tags_filter($tags, $separator));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean an array of tags: trim + remove empty entries
|
||||||
|
*
|
||||||
|
* @param array|null $tags List of tags
|
||||||
|
* @param string $separator
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
function tags_filter(?array $tags, string $separator): array
|
||||||
|
{
|
||||||
|
$trimDefault = " \t\n\r\0\x0B";
|
||||||
|
return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
|
||||||
|
return trim($entry, $trimDefault . $separator);
|
||||||
|
}, $tags ?? [])));
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Bookmark\Exception;
|
namespace Shaarli\Bookmark\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Bookmark\Exception;
|
namespace Shaarli\Bookmark\Exception;
|
||||||
|
|
||||||
|
class EmptyDataStoreException extends \Exception
|
||||||
class EmptyDataStoreException extends \Exception {}
|
{
|
||||||
|
}
|
||||||
|
|
|
@ -16,14 +16,14 @@ public function __construct($bookmark)
|
||||||
} else {
|
} else {
|
||||||
$created = 'Not a DateTime object';
|
$created = 'Not a DateTime object';
|
||||||
}
|
}
|
||||||
$this->message = 'This bookmark is not valid'. PHP_EOL;
|
$this->message = 'This bookmark is not valid' . PHP_EOL;
|
||||||
$this->message .= ' - ID: '. $bookmark->getId() . PHP_EOL;
|
$this->message .= ' - ID: ' . $bookmark->getId() . PHP_EOL;
|
||||||
$this->message .= ' - Title: '. $bookmark->getTitle() . PHP_EOL;
|
$this->message .= ' - Title: ' . $bookmark->getTitle() . PHP_EOL;
|
||||||
$this->message .= ' - Url: '. $bookmark->getUrl() . PHP_EOL;
|
$this->message .= ' - Url: ' . $bookmark->getUrl() . PHP_EOL;
|
||||||
$this->message .= ' - ShortUrl: '. $bookmark->getShortUrl() . PHP_EOL;
|
$this->message .= ' - ShortUrl: ' . $bookmark->getShortUrl() . PHP_EOL;
|
||||||
$this->message .= ' - Created: '. $created . PHP_EOL;
|
$this->message .= ' - Created: ' . $created . PHP_EOL;
|
||||||
} else {
|
} else {
|
||||||
$this->message = 'The provided data is not a bookmark'. PHP_EOL;
|
$this->message = 'The provided data is not a bookmark' . PHP_EOL;
|
||||||
$this->message .= var_export($bookmark, true);
|
$this->message .= var_export($bookmark, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Bookmark\Exception;
|
namespace Shaarli\Bookmark\Exception;
|
||||||
|
|
||||||
|
|
||||||
class NotWritableDataStoreException extends \Exception
|
class NotWritableDataStoreException extends \Exception
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -13,7 +11,7 @@ class NotWritableDataStoreException extends \Exception
|
||||||
*/
|
*/
|
||||||
public function __construct($dataStore)
|
public function __construct($dataStore)
|
||||||
{
|
{
|
||||||
$this->message = 'Couldn\'t load data from the data store file "'. $dataStore .'". '.
|
$this->message = 'Couldn\'t load data from the data store file "' . $dataStore . '". ' .
|
||||||
'Your data might be corrupted, or your file isn\'t readable.';
|
'Your data might be corrupted, or your file isn\'t readable.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Config;
|
namespace Shaarli\Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -19,7 +19,7 @@ public function read($filepath)
|
||||||
$data = file_get_contents($filepath);
|
$data = file_get_contents($filepath);
|
||||||
$data = str_replace(self::getPhpHeaders(), '', $data);
|
$data = str_replace(self::getPhpHeaders(), '', $data);
|
||||||
$data = str_replace(self::getPhpSuffix(), '', $data);
|
$data = str_replace(self::getPhpSuffix(), '', $data);
|
||||||
$data = json_decode($data, true);
|
$data = json_decode(trim($data), true);
|
||||||
if ($data === null) {
|
if ($data === null) {
|
||||||
$errorCode = json_last_error();
|
$errorCode = json_last_error();
|
||||||
$error = sprintf(
|
$error = sprintf(
|
||||||
|
@ -73,7 +73,7 @@ public function getExtension()
|
||||||
*/
|
*/
|
||||||
public static function getPhpHeaders()
|
public static function getPhpHeaders()
|
||||||
{
|
{
|
||||||
return '<?php /*'. PHP_EOL;
|
return '<?php /*';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,6 +85,6 @@ public static function getPhpHeaders()
|
||||||
*/
|
*/
|
||||||
public static function getPhpSuffix()
|
public static function getPhpSuffix()
|
||||||
{
|
{
|
||||||
return PHP_EOL . '*/ ?>';
|
return '*/ ?>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Config;
|
namespace Shaarli\Config;
|
||||||
|
|
||||||
use Shaarli\Config\Exception\MissingFieldConfigException;
|
use Shaarli\Config\Exception\MissingFieldConfigException;
|
||||||
|
@ -20,7 +21,7 @@ class ConfigManager
|
||||||
*/
|
*/
|
||||||
protected static $NOT_FOUND = 'NOT_FOUND';
|
protected static $NOT_FOUND = 'NOT_FOUND';
|
||||||
|
|
||||||
public static $DEFAULT_PLUGINS = array('qrcode');
|
public static $DEFAULT_PLUGINS = ['qrcode'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string Config folder.
|
* @var string Config folder.
|
||||||
|
@ -133,7 +134,7 @@ public function get($setting, $default = '')
|
||||||
public function set($setting, $value, $write = false, $isLoggedIn = false)
|
public function set($setting, $value, $write = false, $isLoggedIn = false)
|
||||||
{
|
{
|
||||||
if (empty($setting) || ! is_string($setting)) {
|
if (empty($setting) || ! is_string($setting)) {
|
||||||
throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
|
throw new \Exception(t('Invalid setting key parameter. String expected, got: ') . gettype($setting));
|
||||||
}
|
}
|
||||||
|
|
||||||
// During the ConfigIO transition, map legacy settings to the new ones.
|
// During the ConfigIO transition, map legacy settings to the new ones.
|
||||||
|
@ -160,7 +161,7 @@ public function set($setting, $value, $write = false, $isLoggedIn = false)
|
||||||
public function remove($setting, $write = false, $isLoggedIn = false)
|
public function remove($setting, $write = false, $isLoggedIn = false)
|
||||||
{
|
{
|
||||||
if (empty($setting) || ! is_string($setting)) {
|
if (empty($setting) || ! is_string($setting)) {
|
||||||
throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
|
throw new \Exception(t('Invalid setting key parameter. String expected, got: ') . gettype($setting));
|
||||||
}
|
}
|
||||||
|
|
||||||
// During the ConfigIO transition, map legacy settings to the new ones.
|
// During the ConfigIO transition, map legacy settings to the new ones.
|
||||||
|
@ -213,7 +214,7 @@ public function exists($setting)
|
||||||
public function write($isLoggedIn)
|
public function write($isLoggedIn)
|
||||||
{
|
{
|
||||||
// These fields are required in configuration.
|
// These fields are required in configuration.
|
||||||
$mandatoryFields = array(
|
$mandatoryFields = [
|
||||||
'credentials.login',
|
'credentials.login',
|
||||||
'credentials.hash',
|
'credentials.hash',
|
||||||
'credentials.salt',
|
'credentials.salt',
|
||||||
|
@ -222,7 +223,7 @@ public function write($isLoggedIn)
|
||||||
'general.title',
|
'general.title',
|
||||||
'general.header_link',
|
'general.header_link',
|
||||||
'privacy.default_private_links',
|
'privacy.default_private_links',
|
||||||
);
|
];
|
||||||
|
|
||||||
// Only logged in user can alter config.
|
// Only logged in user can alter config.
|
||||||
if (is_file($this->getConfigFileExt()) && !$isLoggedIn) {
|
if (is_file($this->getConfigFileExt()) && !$isLoggedIn) {
|
||||||
|
@ -366,10 +367,12 @@ protected function setDefaultValues()
|
||||||
$this->setEmpty('general.links_per_page', 20);
|
$this->setEmpty('general.links_per_page', 20);
|
||||||
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
|
||||||
$this->setEmpty('general.default_note_title', 'Note: ');
|
$this->setEmpty('general.default_note_title', 'Note: ');
|
||||||
$this->setEmpty('general.retrieve_description', false);
|
$this->setEmpty('general.retrieve_description', true);
|
||||||
|
$this->setEmpty('general.enable_async_metadata', true);
|
||||||
|
$this->setEmpty('general.tags_separator', ' ');
|
||||||
|
|
||||||
$this->setEmpty('updates.check_updates', false);
|
$this->setEmpty('updates.check_updates', true);
|
||||||
$this->setEmpty('updates.check_updates_branch', 'stable');
|
$this->setEmpty('updates.check_updates_branch', 'latest');
|
||||||
$this->setEmpty('updates.check_updates_interval', 86400);
|
$this->setEmpty('updates.check_updates_interval', 86400);
|
||||||
|
|
||||||
$this->setEmpty('feed.rss_permalinks', true);
|
$this->setEmpty('feed.rss_permalinks', true);
|
||||||
|
@ -390,7 +393,7 @@ protected function setDefaultValues()
|
||||||
$this->setEmpty('translation.mode', 'php');
|
$this->setEmpty('translation.mode', 'php');
|
||||||
$this->setEmpty('translation.extensions', []);
|
$this->setEmpty('translation.extensions', []);
|
||||||
|
|
||||||
$this->setEmpty('plugins', array());
|
$this->setEmpty('plugins', []);
|
||||||
|
|
||||||
$this->setEmpty('formatter', 'markdown');
|
$this->setEmpty('formatter', 'markdown');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Config;
|
namespace Shaarli\Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +13,7 @@ class ConfigPhp implements ConfigIO
|
||||||
/**
|
/**
|
||||||
* @var array List of config key without group.
|
* @var array List of config key without group.
|
||||||
*/
|
*/
|
||||||
public static $ROOT_KEYS = array(
|
public static $ROOT_KEYS = [
|
||||||
'login',
|
'login',
|
||||||
'hash',
|
'hash',
|
||||||
'salt',
|
'salt',
|
||||||
|
@ -22,7 +23,7 @@ class ConfigPhp implements ConfigIO
|
||||||
'redirector',
|
'redirector',
|
||||||
'disablesessionprotection',
|
'disablesessionprotection',
|
||||||
'privateLinkByDefault',
|
'privateLinkByDefault',
|
||||||
);
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map legacy config keys with the new ones.
|
* Map legacy config keys with the new ones.
|
||||||
|
@ -31,7 +32,7 @@ class ConfigPhp implements ConfigIO
|
||||||
*
|
*
|
||||||
* @var array current key => legacy key.
|
* @var array current key => legacy key.
|
||||||
*/
|
*/
|
||||||
public static $LEGACY_KEYS_MAPPING = array(
|
public static $LEGACY_KEYS_MAPPING = [
|
||||||
'credentials.login' => 'login',
|
'credentials.login' => 'login',
|
||||||
'credentials.hash' => 'hash',
|
'credentials.hash' => 'hash',
|
||||||
'credentials.salt' => 'salt',
|
'credentials.salt' => 'salt',
|
||||||
|
@ -68,7 +69,7 @@ class ConfigPhp implements ConfigIO
|
||||||
'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS',
|
'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS',
|
||||||
'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS',
|
'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS',
|
||||||
'security.open_shaarli' => 'config.OPEN_SHAARLI',
|
'security.open_shaarli' => 'config.OPEN_SHAARLI',
|
||||||
);
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
@ -76,12 +77,12 @@ class ConfigPhp implements ConfigIO
|
||||||
public function read($filepath)
|
public function read($filepath)
|
||||||
{
|
{
|
||||||
if (! file_exists($filepath) || ! is_readable($filepath)) {
|
if (! file_exists($filepath) || ! is_readable($filepath)) {
|
||||||
return array();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
include $filepath;
|
include $filepath;
|
||||||
|
|
||||||
$out = array();
|
$out = [];
|
||||||
foreach (self::$ROOT_KEYS as $key) {
|
foreach (self::$ROOT_KEYS as $key) {
|
||||||
$out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : '';
|
$out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : '';
|
||||||
}
|
}
|
||||||
|
@ -95,7 +96,7 @@ public function read($filepath)
|
||||||
*/
|
*/
|
||||||
public function write($filepath, $conf)
|
public function write($filepath, $conf)
|
||||||
{
|
{
|
||||||
$configStr = '<?php '. PHP_EOL;
|
$configStr = '<?php ' . PHP_EOL;
|
||||||
foreach (self::$ROOT_KEYS as $key) {
|
foreach (self::$ROOT_KEYS as $key) {
|
||||||
if (isset($conf[$key])) {
|
if (isset($conf[$key])) {
|
||||||
$configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
|
$configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
|
||||||
|
@ -106,8 +107,8 @@ public function write($filepath, $conf)
|
||||||
foreach ($conf['config'] as $key => $value) {
|
foreach ($conf['config'] as $key => $value) {
|
||||||
$configStr .= '$GLOBALS[\'config\'][\''
|
$configStr .= '$GLOBALS[\'config\'][\''
|
||||||
. $key
|
. $key
|
||||||
.'\'] = '
|
. '\'] = '
|
||||||
.var_export($conf['config'][$key], true).';'
|
. var_export($conf['config'][$key], true) . ';'
|
||||||
. PHP_EOL;
|
. PHP_EOL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,18 +116,19 @@ public function write($filepath, $conf)
|
||||||
foreach ($conf['plugins'] as $key => $value) {
|
foreach ($conf['plugins'] as $key => $value) {
|
||||||
$configStr .= '$GLOBALS[\'plugins\'][\''
|
$configStr .= '$GLOBALS[\'plugins\'][\''
|
||||||
. $key
|
. $key
|
||||||
.'\'] = '
|
. '\'] = '
|
||||||
.var_export($conf['plugins'][$key], true).';'
|
. var_export($conf['plugins'][$key], true) . ';'
|
||||||
. PHP_EOL;
|
. PHP_EOL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file_put_contents($filepath, $configStr)
|
if (
|
||||||
|
!file_put_contents($filepath, $configStr)
|
||||||
|| strcmp(file_get_contents($filepath), $configStr) != 0
|
|| strcmp(file_get_contents($filepath), $configStr) != 0
|
||||||
) {
|
) {
|
||||||
throw new \Shaarli\Exceptions\IOException(
|
throw new \Shaarli\Exceptions\IOException(
|
||||||
$filepath,
|
$filepath,
|
||||||
t('Shaarli could not create the config file. '.
|
t('Shaarli could not create the config file. ' .
|
||||||
'Please make sure Shaarli has the right to write in the folder is it installed in.')
|
'Please make sure Shaarli has the right to write in the folder is it installed in.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,8 +39,8 @@ function ($value, string $key) use ($directories) {
|
||||||
throw new PluginConfigOrderException();
|
throw new PluginConfigOrderException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$plugins = array();
|
$plugins = [];
|
||||||
$newEnabledPlugins = array();
|
$newEnabledPlugins = [];
|
||||||
foreach ($formData as $key => $data) {
|
foreach ($formData as $key => $data) {
|
||||||
if (startsWith($key, 'order')) {
|
if (startsWith($key, 'order')) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -62,7 +62,7 @@ function ($value, string $key) use ($directories) {
|
||||||
throw new PluginConfigOrderException();
|
throw new PluginConfigOrderException();
|
||||||
}
|
}
|
||||||
|
|
||||||
$finalPlugins = array();
|
$finalPlugins = [];
|
||||||
// Make plugins order continuous.
|
// Make plugins order continuous.
|
||||||
foreach ($plugins as $plugin) {
|
foreach ($plugins as $plugin) {
|
||||||
$finalPlugins[] = $plugin;
|
$finalPlugins[] = $plugin;
|
||||||
|
@ -81,7 +81,7 @@ function ($value, string $key) use ($directories) {
|
||||||
*/
|
*/
|
||||||
function validate_plugin_order($formData)
|
function validate_plugin_order($formData)
|
||||||
{
|
{
|
||||||
$orders = array();
|
$orders = [];
|
||||||
foreach ($formData as $key => $value) {
|
foreach ($formData as $key => $value) {
|
||||||
// No duplicate order allowed.
|
// No duplicate order allowed.
|
||||||
if (in_array($value, $orders, true)) {
|
if (in_array($value, $orders, true)) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Config\Exception;
|
namespace Shaarli\Config\Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Config\Exception;
|
namespace Shaarli\Config\Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
namespace Shaarli\Container;
|
namespace Shaarli\Container;
|
||||||
|
|
||||||
use malkusch\lock\mutex\FlockMutex;
|
use malkusch\lock\mutex\FlockMutex;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shaarli\Bookmark\BookmarkFileService;
|
use Shaarli\Bookmark\BookmarkFileService;
|
||||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
use Shaarli\Front\Controller\Visitor\ErrorNotFoundController;
|
||||||
use Shaarli\History;
|
use Shaarli\History;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||||
use Shaarli\Plugin\PluginManager;
|
use Shaarli\Plugin\PluginManager;
|
||||||
use Shaarli\Render\PageBuilder;
|
use Shaarli\Render\PageBuilder;
|
||||||
|
@ -48,6 +50,12 @@ class ContainerBuilder
|
||||||
/** @var LoginManager */
|
/** @var LoginManager */
|
||||||
protected $login;
|
protected $login;
|
||||||
|
|
||||||
|
/** @var PluginManager */
|
||||||
|
protected $pluginManager;
|
||||||
|
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
/** @var string|null */
|
/** @var string|null */
|
||||||
protected $basePath = null;
|
protected $basePath = null;
|
||||||
|
|
||||||
|
@ -55,12 +63,16 @@ public function __construct(
|
||||||
ConfigManager $conf,
|
ConfigManager $conf,
|
||||||
SessionManager $session,
|
SessionManager $session,
|
||||||
CookieManager $cookieManager,
|
CookieManager $cookieManager,
|
||||||
LoginManager $login
|
LoginManager $login,
|
||||||
|
PluginManager $pluginManager,
|
||||||
|
LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
$this->conf = $conf;
|
$this->conf = $conf;
|
||||||
$this->session = $session;
|
$this->session = $session;
|
||||||
$this->login = $login;
|
$this->login = $login;
|
||||||
$this->cookieManager = $cookieManager;
|
$this->cookieManager = $cookieManager;
|
||||||
|
$this->pluginManager = $pluginManager;
|
||||||
|
$this->logger = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function build(): ShaarliContainer
|
public function build(): ShaarliContainer
|
||||||
|
@ -71,11 +83,10 @@ public function build(): ShaarliContainer
|
||||||
$container['sessionManager'] = $this->session;
|
$container['sessionManager'] = $this->session;
|
||||||
$container['cookieManager'] = $this->cookieManager;
|
$container['cookieManager'] = $this->cookieManager;
|
||||||
$container['loginManager'] = $this->login;
|
$container['loginManager'] = $this->login;
|
||||||
|
$container['pluginManager'] = $this->pluginManager;
|
||||||
|
$container['logger'] = $this->logger;
|
||||||
$container['basePath'] = $this->basePath;
|
$container['basePath'] = $this->basePath;
|
||||||
|
|
||||||
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
|
|
||||||
return new PluginManager($container->conf);
|
|
||||||
};
|
|
||||||
|
|
||||||
$container['history'] = function (ShaarliContainer $container): History {
|
$container['history'] = function (ShaarliContainer $container): History {
|
||||||
return new History($container->conf->get('resource.history'));
|
return new History($container->conf->get('resource.history'));
|
||||||
|
@ -90,24 +101,21 @@ public function build(): ShaarliContainer
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
|
||||||
|
return new MetadataRetriever($container->conf, $container->httpAccess);
|
||||||
|
};
|
||||||
|
|
||||||
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
|
$container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
|
||||||
return new PageBuilder(
|
return new PageBuilder(
|
||||||
$container->conf,
|
$container->conf,
|
||||||
$container->sessionManager->getSession(),
|
$container->sessionManager->getSession(),
|
||||||
|
$container->logger,
|
||||||
$container->bookmarkService,
|
$container->bookmarkService,
|
||||||
$container->sessionManager->generateToken(),
|
$container->sessionManager->generateToken(),
|
||||||
$container->loginManager->isLoggedIn()
|
$container->loginManager->isLoggedIn()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
|
|
||||||
$pluginManager = new PluginManager($container->conf);
|
|
||||||
|
|
||||||
$pluginManager->load($container->conf->get('general.enabled_plugins'));
|
|
||||||
|
|
||||||
return $pluginManager;
|
|
||||||
};
|
|
||||||
|
|
||||||
$container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
|
$container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
|
||||||
return new FormatterFactory(
|
return new FormatterFactory(
|
||||||
$container->conf,
|
$container->conf,
|
||||||
|
@ -145,7 +153,7 @@ public function build(): ShaarliContainer
|
||||||
|
|
||||||
$container['updater'] = function (ShaarliContainer $container): Updater {
|
$container['updater'] = function (ShaarliContainer $container): Updater {
|
||||||
return new Updater(
|
return new Updater(
|
||||||
UpdaterUtils::read_updates_file($container->conf->get('resource.updates')),
|
UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')),
|
||||||
$container->bookmarkService,
|
$container->bookmarkService,
|
||||||
$container->conf,
|
$container->conf,
|
||||||
$container->loginManager->isLoggedIn()
|
$container->loginManager->isLoggedIn()
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
|
|
||||||
namespace Shaarli\Container;
|
namespace Shaarli\Container;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
use Shaarli\Feed\FeedBuilder;
|
use Shaarli\Feed\FeedBuilder;
|
||||||
use Shaarli\Formatter\FormatterFactory;
|
use Shaarli\Formatter\FormatterFactory;
|
||||||
use Shaarli\History;
|
use Shaarli\History;
|
||||||
use Shaarli\Http\HttpAccess;
|
use Shaarli\Http\HttpAccess;
|
||||||
|
use Shaarli\Http\MetadataRetriever;
|
||||||
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
use Shaarli\Netscape\NetscapeBookmarkUtils;
|
||||||
use Shaarli\Plugin\PluginManager;
|
use Shaarli\Plugin\PluginManager;
|
||||||
use Shaarli\Render\PageBuilder;
|
use Shaarli\Render\PageBuilder;
|
||||||
|
@ -35,6 +37,8 @@
|
||||||
* @property History $history
|
* @property History $history
|
||||||
* @property HttpAccess $httpAccess
|
* @property HttpAccess $httpAccess
|
||||||
* @property LoginManager $loginManager
|
* @property LoginManager $loginManager
|
||||||
|
* @property LoggerInterface $logger
|
||||||
|
* @property MetadataRetriever $metadataRetriever
|
||||||
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
|
* @property NetscapeBookmarkUtils $netscapeBookmarkUtils
|
||||||
* @property callable $notFoundHandler Overrides default Slim exception display
|
* @property callable $notFoundHandler Overrides default Slim exception display
|
||||||
* @property PageBuilder $pageBuilder
|
* @property PageBuilder $pageBuilder
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Exceptions;
|
namespace Shaarli\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
|
@ -1,34 +1,43 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shaarli\Feed;
|
namespace Shaarli\Feed;
|
||||||
|
|
||||||
|
use DatePeriod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple cache system, mainly for the RSS/ATOM feeds
|
* Simple cache system, mainly for the RSS/ATOM feeds
|
||||||
*/
|
*/
|
||||||
class CachedPage
|
class CachedPage
|
||||||
{
|
{
|
||||||
// Directory containing page caches
|
/** Directory containing page caches */
|
||||||
private $cacheDir;
|
protected $cacheDir;
|
||||||
|
|
||||||
// Should this URL be cached (boolean)?
|
/** Should this URL be cached (boolean)? */
|
||||||
private $shouldBeCached;
|
protected $shouldBeCached;
|
||||||
|
|
||||||
// Name of the cache file for this URL
|
/** Name of the cache file for this URL */
|
||||||
private $filename;
|
protected $filename;
|
||||||
|
|
||||||
|
/** @var DatePeriod|null Optionally specify a period of time for cache validity */
|
||||||
|
protected $validityPeriod;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new CachedPage
|
* Creates a new CachedPage
|
||||||
*
|
*
|
||||||
* @param string $cacheDir page cache directory
|
* @param string $cacheDir page cache directory
|
||||||
* @param string $url page URL
|
* @param string $url page URL
|
||||||
* @param bool $shouldBeCached whether this page needs to be cached
|
* @param bool $shouldBeCached whether this page needs to be cached
|
||||||
|
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
|
||||||
*/
|
*/
|
||||||
public function __construct($cacheDir, $url, $shouldBeCached)
|
public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
|
||||||
{
|
{
|
||||||
// TODO: check write access to the cache directory
|
// TODO: check write access to the cache directory
|
||||||
$this->cacheDir = $cacheDir;
|
$this->cacheDir = $cacheDir;
|
||||||
$this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
|
$this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
|
||||||
$this->shouldBeCached = $shouldBeCached;
|
$this->shouldBeCached = $shouldBeCached;
|
||||||
|
$this->validityPeriod = $validityPeriod;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,10 +50,20 @@ public function cachedVersion()
|
||||||
if (!$this->shouldBeCached) {
|
if (!$this->shouldBeCached) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (is_file($this->filename)) {
|
if (!is_file($this->filename)) {
|
||||||
return file_get_contents($this->filename);
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
if ($this->validityPeriod !== null) {
|
||||||
|
$cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
|
||||||
|
if (
|
||||||
|
$cacheDate < $this->validityPeriod->getStartDate()
|
||||||
|
|| $cacheDate > $this->validityPeriod->getEndDate()
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_get_contents($this->filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Feed;
|
namespace Shaarli\Feed;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
@ -107,14 +108,14 @@ public function buildData(string $feedType, ?array $userInput)
|
||||||
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
|
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
|
||||||
|
|
||||||
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
|
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
|
||||||
$keys = array();
|
$keys = [];
|
||||||
foreach ($linksToDisplay as $key => $value) {
|
foreach ($linksToDisplay as $key => $value) {
|
||||||
$keys[] = $key;
|
$keys[] = $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
$pageaddr = escape(index_url($this->serverInfo));
|
$pageaddr = escape(index_url($this->serverInfo));
|
||||||
$this->formatter->addContextData('index_url', $pageaddr);
|
$this->formatter->addContextData('index_url', $pageaddr);
|
||||||
$linkDisplayed = array();
|
$linkDisplayed = [];
|
||||||
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
|
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
|
||||||
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
|
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
|
||||||
}
|
}
|
||||||
|
@ -176,9 +177,9 @@ protected function buildItem(string $feedType, $link, $pageaddr)
|
||||||
$data = $this->formatter->format($link);
|
$data = $this->formatter->format($link);
|
||||||
$data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
|
$data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
|
||||||
if ($this->usePermalinks === true) {
|
if ($this->usePermalinks === true) {
|
||||||
$permalink = '<a href="'. $data['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
|
$permalink = '<a href="' . $data['url'] . '" title="' . t('Direct link') . '">' . t('Direct link') . '</a>';
|
||||||
} else {
|
} else {
|
||||||
$permalink = '<a href="'. $data['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
|
$permalink = '<a href="' . $data['guid'] . '" title="' . t('Permalink') . '">' . t('Permalink') . '</a>';
|
||||||
}
|
}
|
||||||
$data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $permalink;
|
$data['description'] .= PHP_EOL . PHP_EOL . '<br>— ' . $permalink;
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
*/
|
*/
|
||||||
class BookmarkDefaultFormatter extends BookmarkFormatter
|
class BookmarkDefaultFormatter extends BookmarkFormatter
|
||||||
{
|
{
|
||||||
const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
|
protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT';
|
||||||
const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|';
|
protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritdoc
|
* @inheritdoc
|
||||||
|
@ -46,8 +46,13 @@ protected function formatDescription($bookmark)
|
||||||
$bookmark->getDescription() ?? '',
|
$bookmark->getDescription() ?? '',
|
||||||
$bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
|
$bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
|
||||||
);
|
);
|
||||||
|
$description = format_description(
|
||||||
|
escape($description),
|
||||||
|
$indexUrl,
|
||||||
|
$this->conf->get('formatter_settings.autolink', true)
|
||||||
|
);
|
||||||
|
|
||||||
return $this->replaceTokens(format_description(escape($description), $indexUrl));
|
return $this->replaceTokens($description);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,15 +68,16 @@ protected function formatTagList($bookmark)
|
||||||
*/
|
*/
|
||||||
protected function formatTagListHtml($bookmark)
|
protected function formatTagListHtml($bookmark)
|
||||||
{
|
{
|
||||||
|
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
|
||||||
if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
|
if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
|
||||||
return $this->formatTagList($bookmark);
|
return $this->formatTagList($bookmark);
|
||||||
}
|
}
|
||||||
|
|
||||||
$tags = $this->tokenizeSearchHighlightField(
|
$tags = $this->tokenizeSearchHighlightField(
|
||||||
$bookmark->getTagsString(),
|
$bookmark->getTagsString($tagsSeparator),
|
||||||
$bookmark->getAdditionalContentEntry('search_highlight')['tags']
|
$bookmark->getAdditionalContentEntry('search_highlight')['tags']
|
||||||
);
|
);
|
||||||
$tags = $this->filterTagList(explode(' ', $tags));
|
$tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator));
|
||||||
$tags = escape($tags);
|
$tags = escape($tags);
|
||||||
$tags = $this->replaceTokensArray($tags);
|
$tags = $this->replaceTokensArray($tags);
|
||||||
|
|
||||||
|
@ -83,7 +89,7 @@ protected function formatTagListHtml($bookmark)
|
||||||
*/
|
*/
|
||||||
protected function formatTagString($bookmark)
|
protected function formatTagString($bookmark)
|
||||||
{
|
{
|
||||||
return implode(' ', $this->formatTagList($bookmark));
|
return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -267,7 +267,7 @@ protected function formatTagListHtml($bookmark)
|
||||||
*/
|
*/
|
||||||
protected function formatTagString($bookmark)
|
protected function formatTagString($bookmark)
|
||||||
{
|
{
|
||||||
return implode(' ', $this->formatTagList($bookmark));
|
return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -351,6 +351,7 @@ protected function formatUpdatedTimestamp(Bookmark $bookmark)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format tag list, e.g. remove private tags if the user is not logged in.
|
* Format tag list, e.g. remove private tags if the user is not logged in.
|
||||||
|
* TODO: this method is called multiple time to format tags, the result should be cached.
|
||||||
*
|
*
|
||||||
* @param array $tags
|
* @param array $tags
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,7 +16,7 @@ class BookmarkMarkdownFormatter extends BookmarkDefaultFormatter
|
||||||
/**
|
/**
|
||||||
* When this tag is present in a bookmark, its description should not be processed with Markdown
|
* When this tag is present in a bookmark, its description should not be processed with Markdown
|
||||||
*/
|
*/
|
||||||
const NO_MD_TAG = 'nomarkdown';
|
public const NO_MD_TAG = 'nomarkdown';
|
||||||
|
|
||||||
/** @var \Parsedown instance */
|
/** @var \Parsedown instance */
|
||||||
protected $parsedown;
|
protected $parsedown;
|
||||||
|
@ -71,7 +71,7 @@ public function formatDescription($bookmark)
|
||||||
$processedDescription = $this->replaceTokens($processedDescription);
|
$processedDescription = $this->replaceTokens($processedDescription);
|
||||||
|
|
||||||
if (!empty($processedDescription)) {
|
if (!empty($processedDescription)) {
|
||||||
$processedDescription = '<div class="markdown">'. $processedDescription . '</div>';
|
$processedDescription = '<div class="markdown">' . $processedDescription . '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $processedDescription;
|
return $processedDescription;
|
||||||
|
@ -110,7 +110,7 @@ protected function filterProtocols($description)
|
||||||
function ($match) use ($allowedProtocols, $indexUrl) {
|
function ($match) use ($allowedProtocols, $indexUrl) {
|
||||||
$link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
|
$link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
|
||||||
$link .= whitelist_protocols($match[1], $allowedProtocols);
|
$link .= whitelist_protocols($match[1], $allowedProtocols);
|
||||||
return ']('. $link.')';
|
return '](' . $link . ')';
|
||||||
},
|
},
|
||||||
$description
|
$description
|
||||||
);
|
);
|
||||||
|
@ -137,7 +137,7 @@ protected function formatHashTags($description)
|
||||||
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
* \p{Mn} - any non marking space (accents, umlauts, etc)
|
||||||
*/
|
*/
|
||||||
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
|
||||||
$replacement = '$1[#$2]('. $indexUrl .'./add-tag/$2)';
|
$replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)';
|
||||||
|
|
||||||
$descriptionLines = explode(PHP_EOL, $description);
|
$descriptionLines = explode(PHP_EOL, $description);
|
||||||
$descriptionOut = '';
|
$descriptionOut = '';
|
||||||
|
@ -178,17 +178,17 @@ protected function formatHashTags($description)
|
||||||
*/
|
*/
|
||||||
protected function sanitizeHtml($description)
|
protected function sanitizeHtml($description)
|
||||||
{
|
{
|
||||||
$escapeTags = array(
|
$escapeTags = [
|
||||||
'script',
|
'script',
|
||||||
'style',
|
'style',
|
||||||
'link',
|
'link',
|
||||||
'iframe',
|
'iframe',
|
||||||
'frameset',
|
'frameset',
|
||||||
'frame',
|
'frame',
|
||||||
);
|
];
|
||||||
foreach ($escapeTags as $tag) {
|
foreach ($escapeTags as $tag) {
|
||||||
$description = preg_replace_callback(
|
$description = preg_replace_callback(
|
||||||
'#<\s*'. $tag .'[^>]*>(.*</\s*'. $tag .'[^>]*>)?#is',
|
'#<\s*' . $tag . '[^>]*>(.*</\s*' . $tag . '[^>]*>)?#is',
|
||||||
function ($match) {
|
function ($match) {
|
||||||
return escape($match[0]);
|
return escape($match[0]);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,4 +10,6 @@
|
||||||
*
|
*
|
||||||
* @package Shaarli\Formatter
|
* @package Shaarli\Formatter
|
||||||
*/
|
*/
|
||||||
class BookmarkRawFormatter extends BookmarkFormatter {}
|
class BookmarkRawFormatter extends BookmarkFormatter
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ public function __construct(ConfigManager $conf, bool $isLoggedIn)
|
||||||
public function getFormatter(string $type = null): BookmarkFormatter
|
public function getFormatter(string $type = null): BookmarkFormatter
|
||||||
{
|
{
|
||||||
$type = $type ? $type : $this->conf->get('formatter', 'default');
|
$type = $type ? $type : $this->conf->get('formatter', 'default');
|
||||||
$className = '\\Shaarli\\Formatter\\Bookmark'. ucfirst($type) .'Formatter';
|
$className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter';
|
||||||
if (!class_exists($className)) {
|
if (!class_exists($className)) {
|
||||||
$className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
|
$className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,8 @@ public function __invoke(Request $request, Response $response, callable $next):
|
||||||
$this->initBasePath($request);
|
$this->initBasePath($request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!is_file($this->container->conf->getConfigFileExt())
|
if (
|
||||||
|
!is_file($this->container->conf->getConfigFileExt())
|
||||||
&& !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
|
&& !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
|
||||||
) {
|
) {
|
||||||
return $response->withRedirect($this->container->basePath . '/install');
|
return $response->withRedirect($this->container->basePath . '/install');
|
||||||
|
@ -86,7 +87,8 @@ protected function runUpdates(): void
|
||||||
*/
|
*/
|
||||||
protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
|
protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
|
||||||
{
|
{
|
||||||
if (// if the user isn't logged in
|
if (
|
||||||
|
// if the user isn't logged in
|
||||||
!$this->container->loginManager->isLoggedIn()
|
!$this->container->loginManager->isLoggedIn()
|
||||||
// and Shaarli doesn't have public content...
|
// and Shaarli doesn't have public content...
|
||||||
&& $this->container->conf->get('privacy.hide_public_links')
|
&& $this->container->conf->get('privacy.hide_public_links')
|
||||||
|
|
|
@ -51,7 +51,10 @@ public function index(Request $request, Response $response): Response
|
||||||
$this->assignView('languages', Languages::getAvailableLanguages());
|
$this->assignView('languages', Languages::getAvailableLanguages());
|
||||||
$this->assignView('gd_enabled', extension_loaded('gd'));
|
$this->assignView('gd_enabled', extension_loaded('gd'));
|
||||||
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
|
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
|
||||||
$this->assignView('pagetitle', t('Configure') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
|
$this->assignView(
|
||||||
|
'pagetitle',
|
||||||
|
t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
|
);
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::CONFIGURE));
|
return $response->write($this->render(TemplatePage::CONFIGURE));
|
||||||
}
|
}
|
||||||
|
@ -95,12 +98,15 @@ public function save(Request $request, Response $response): Response
|
||||||
}
|
}
|
||||||
|
|
||||||
$thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
|
$thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
|
||||||
if ($thumbnailsMode !== Thumbnailer::MODE_NONE
|
if (
|
||||||
|
$thumbnailsMode !== Thumbnailer::MODE_NONE
|
||||||
&& $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
|
&& $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
|
||||||
) {
|
) {
|
||||||
$this->saveWarningMessage(
|
$this->saveWarningMessage(
|
||||||
t('You have enabled or changed thumbnails mode.') .
|
t('You have enabled or changed thumbnails mode.') .
|
||||||
'<a href="'. $this->container->basePath .'/admin/thumbnails">' . t('Please synchronize them.') .'</a>'
|
'<a href="' . $this->container->basePath . '/admin/thumbnails">' .
|
||||||
|
t('Please synchronize them.') .
|
||||||
|
'</a>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
$this->container->conf->set('thumbnails.mode', $thumbnailsMode);
|
$this->container->conf->set('thumbnails.mode', $thumbnailsMode);
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ExportController extends ShaarliAdminController
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, Response $response): Response
|
public function index(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$this->assignView('pagetitle', t('Export') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
|
$this->assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::EXPORT));
|
return $response->write($this->render(TemplatePage::EXPORT));
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ public function export(Request $request, Response $response): Response
|
||||||
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
|
$response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
$response = $response->withHeader(
|
$response = $response->withHeader(
|
||||||
'Content-disposition',
|
'Content-disposition',
|
||||||
'attachment; filename=bookmarks_'.$selection.'_'.$now->format(Bookmark::LINK_DATE_FORMAT).'.html'
|
'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html'
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assignView('date', $now->format(DateTime::RFC822));
|
$this->assignView('date', $now->format(DateTime::RFC822));
|
||||||
|
|
|
@ -38,7 +38,7 @@ public function index(Request $request, Response $response): Response
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$this->assignView('pagetitle', t('Import') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
|
$this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::IMPORT));
|
return $response->write($this->render(TemplatePage::IMPORT));
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ public function import(Request $request, Response $response): Response
|
||||||
$msg = sprintf(
|
$msg = sprintf(
|
||||||
t(
|
t(
|
||||||
'The file you are trying to upload is probably bigger than what this webserver can accept'
|
'The file you are trying to upload is probably bigger than what this webserver can accept'
|
||||||
.' (%s). Please upload in smaller chunks.'
|
. ' (%s). Please upload in smaller chunks.'
|
||||||
),
|
),
|
||||||
get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
|
get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,371 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shaarli\Front\Controller\Admin;
|
|
||||||
|
|
||||||
use Shaarli\Bookmark\Bookmark;
|
|
||||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
|
||||||
use Shaarli\Formatter\BookmarkMarkdownFormatter;
|
|
||||||
use Shaarli\Render\TemplatePage;
|
|
||||||
use Shaarli\Thumbnailer;
|
|
||||||
use Slim\Http\Request;
|
|
||||||
use Slim\Http\Response;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class PostBookmarkController
|
|
||||||
*
|
|
||||||
* Slim controller used to handle Shaarli create or edit bookmarks.
|
|
||||||
*/
|
|
||||||
class ManageShaareController extends ShaarliAdminController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
|
|
||||||
*/
|
|
||||||
public function addShaare(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$this->assignView(
|
|
||||||
'pagetitle',
|
|
||||||
t('Shaare a new link') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::ADDLINK));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /admin/shaare - Displays the bookmark form for creation.
|
|
||||||
* Note that if the URL is found in existing bookmarks, then it will be in edit mode.
|
|
||||||
*/
|
|
||||||
public function displayCreateForm(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$url = cleanup_url($request->getParam('post'));
|
|
||||||
|
|
||||||
$linkIsNew = false;
|
|
||||||
// Check if URL is not already in database (in this case, we will edit the existing link)
|
|
||||||
$bookmark = $this->container->bookmarkService->findByUrl($url);
|
|
||||||
if (null === $bookmark) {
|
|
||||||
$linkIsNew = true;
|
|
||||||
// Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
|
|
||||||
$title = $request->getParam('title');
|
|
||||||
$description = $request->getParam('description');
|
|
||||||
$tags = $request->getParam('tags');
|
|
||||||
$private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
|
|
||||||
|
|
||||||
// If this is an HTTP(S) link, we try go get the page to extract
|
|
||||||
// the title (otherwise we will to straight to the edit form.)
|
|
||||||
if (empty($title) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
|
|
||||||
$retrieveDescription = $this->container->conf->get('general.retrieve_description');
|
|
||||||
// Short timeout to keep the application responsive
|
|
||||||
// The callback will fill $charset and $title with data from the downloaded page.
|
|
||||||
$this->container->httpAccess->getHttpResponse(
|
|
||||||
$url,
|
|
||||||
$this->container->conf->get('general.download_timeout', 30),
|
|
||||||
$this->container->conf->get('general.download_max_size', 4194304),
|
|
||||||
$this->container->httpAccess->getCurlDownloadCallback(
|
|
||||||
$charset,
|
|
||||||
$title,
|
|
||||||
$description,
|
|
||||||
$tags,
|
|
||||||
$retrieveDescription
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (! empty($title) && strtolower($charset) !== 'utf-8' && mb_check_encoding($charset)) {
|
|
||||||
$title = mb_convert_encoding($title, 'utf-8', $charset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($url) && empty($title)) {
|
|
||||||
$title = $this->container->conf->get('general.default_note_title', t('Note: '));
|
|
||||||
}
|
|
||||||
|
|
||||||
$link = [
|
|
||||||
'title' => $title,
|
|
||||||
'url' => $url ?? '',
|
|
||||||
'description' => $description ?? '',
|
|
||||||
'tags' => $tags ?? '',
|
|
||||||
'private' => $private,
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
$link = $formatter->format($bookmark);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->displayForm($link, $linkIsNew, $request, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
|
|
||||||
*/
|
|
||||||
public function displayEditForm(Request $request, Response $response, array $args): Response
|
|
||||||
{
|
|
||||||
$id = $args['id'] ?? '';
|
|
||||||
try {
|
|
||||||
if (false === ctype_digit($id)) {
|
|
||||||
throw new BookmarkNotFoundException();
|
|
||||||
}
|
|
||||||
$bookmark = $this->container->bookmarkService->get((int) $id); // Read database
|
|
||||||
} catch (BookmarkNotFoundException $e) {
|
|
||||||
$this->saveErrorMessage(sprintf(
|
|
||||||
t('Bookmark with identifier %s could not be found.'),
|
|
||||||
$id
|
|
||||||
));
|
|
||||||
|
|
||||||
return $this->redirect($response, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
$link = $formatter->format($bookmark);
|
|
||||||
|
|
||||||
return $this->displayForm($link, false, $request, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /admin/shaare
|
|
||||||
*/
|
|
||||||
public function save(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$this->checkToken($request);
|
|
||||||
|
|
||||||
// lf_id should only be present if the link exists.
|
|
||||||
$id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
|
|
||||||
if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
|
|
||||||
// Edit
|
|
||||||
$bookmark = $this->container->bookmarkService->get($id);
|
|
||||||
} else {
|
|
||||||
// New link
|
|
||||||
$bookmark = new Bookmark();
|
|
||||||
}
|
|
||||||
|
|
||||||
$bookmark->setTitle($request->getParam('lf_title'));
|
|
||||||
$bookmark->setDescription($request->getParam('lf_description'));
|
|
||||||
$bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
|
|
||||||
$bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
|
|
||||||
$bookmark->setTagsString($request->getParam('lf_tags'));
|
|
||||||
|
|
||||||
if ($this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
|
||||||
&& false === $bookmark->isNote()
|
|
||||||
) {
|
|
||||||
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
|
|
||||||
}
|
|
||||||
$this->container->bookmarkService->addOrSet($bookmark, false);
|
|
||||||
|
|
||||||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
$data = $formatter->format($bookmark);
|
|
||||||
$this->executePageHooks('save_link', $data);
|
|
||||||
|
|
||||||
$bookmark->fromArray($data);
|
|
||||||
$this->container->bookmarkService->set($bookmark);
|
|
||||||
|
|
||||||
// If we are called from the bookmarklet, we must close the popup:
|
|
||||||
if ($request->getParam('source') === 'bookmarklet') {
|
|
||||||
return $response->write('<script>self.close();</script>');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($request->getParam('returnurl'))) {
|
|
||||||
$this->container->environment['HTTP_REFERER'] = escape($request->getParam('returnurl'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectFromReferer(
|
|
||||||
$request,
|
|
||||||
$response,
|
|
||||||
['/admin/add-shaare', '/admin/shaare'], ['addlink', 'post', 'edit_link'],
|
|
||||||
$bookmark->getShortUrl()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
|
|
||||||
*/
|
|
||||||
public function deleteBookmark(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$this->checkToken($request);
|
|
||||||
|
|
||||||
$ids = escape(trim($request->getParam('id') ?? ''));
|
|
||||||
if (empty($ids) || strpos($ids, ' ') !== false) {
|
|
||||||
// multiple, space-separated ids provided
|
|
||||||
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
|
|
||||||
} else {
|
|
||||||
$ids = [$ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert at least one id is given
|
|
||||||
if (0 === count($ids)) {
|
|
||||||
$this->saveErrorMessage(t('Invalid bookmark ID provided.'));
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
$count = 0;
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
try {
|
|
||||||
$bookmark = $this->container->bookmarkService->get((int) $id);
|
|
||||||
} catch (BookmarkNotFoundException $e) {
|
|
||||||
$this->saveErrorMessage(sprintf(
|
|
||||||
t('Bookmark with identifier %s could not be found.'),
|
|
||||||
$id
|
|
||||||
));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = $formatter->format($bookmark);
|
|
||||||
$this->executePageHooks('delete_link', $data);
|
|
||||||
$this->container->bookmarkService->remove($bookmark, false);
|
|
||||||
++ $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count > 0) {
|
|
||||||
$this->container->bookmarkService->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are called from the bookmarklet, we must close the popup:
|
|
||||||
if ($request->getParam('source') === 'bookmarklet') {
|
|
||||||
return $response->write('<script>self.close();</script>');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't redirect to where we were previously because the datastore has changed.
|
|
||||||
return $this->redirect($response, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /admin/shaare/visibility
|
|
||||||
*
|
|
||||||
* Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
|
|
||||||
*/
|
|
||||||
public function changeVisibility(Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$this->checkToken($request);
|
|
||||||
|
|
||||||
$ids = trim(escape($request->getParam('id') ?? ''));
|
|
||||||
if (empty($ids) || strpos($ids, ' ') !== false) {
|
|
||||||
// multiple, space-separated ids provided
|
|
||||||
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
|
|
||||||
} else {
|
|
||||||
// only a single id provided
|
|
||||||
$ids = [$ids];
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert at least one id is given
|
|
||||||
if (0 === count($ids)) {
|
|
||||||
$this->saveErrorMessage(t('Invalid bookmark ID provided.'));
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// assert that the visibility is valid
|
|
||||||
$visibility = $request->getParam('newVisibility');
|
|
||||||
if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
|
|
||||||
$this->saveErrorMessage(t('Invalid visibility provided.'));
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
|
|
||||||
} else {
|
|
||||||
$isPrivate = $visibility === 'private';
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
$count = 0;
|
|
||||||
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
try {
|
|
||||||
$bookmark = $this->container->bookmarkService->get((int) $id);
|
|
||||||
} catch (BookmarkNotFoundException $e) {
|
|
||||||
$this->saveErrorMessage(sprintf(
|
|
||||||
t('Bookmark with identifier %s could not be found.'),
|
|
||||||
$id
|
|
||||||
));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$bookmark->setPrivate($isPrivate);
|
|
||||||
|
|
||||||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
|
||||||
$data = $formatter->format($bookmark);
|
|
||||||
$this->executePageHooks('save_link', $data);
|
|
||||||
$bookmark->fromArray($data);
|
|
||||||
|
|
||||||
$this->container->bookmarkService->set($bookmark, false);
|
|
||||||
++$count;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($count > 0) {
|
|
||||||
$this->container->bookmarkService->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
|
|
||||||
*/
|
|
||||||
public function pinBookmark(Request $request, Response $response, array $args): Response
|
|
||||||
{
|
|
||||||
$this->checkToken($request);
|
|
||||||
|
|
||||||
$id = $args['id'] ?? '';
|
|
||||||
try {
|
|
||||||
if (false === ctype_digit($id)) {
|
|
||||||
throw new BookmarkNotFoundException();
|
|
||||||
}
|
|
||||||
$bookmark = $this->container->bookmarkService->get((int) $id); // Read database
|
|
||||||
} catch (BookmarkNotFoundException $e) {
|
|
||||||
$this->saveErrorMessage(sprintf(
|
|
||||||
t('Bookmark with identifier %s could not be found.'),
|
|
||||||
$id
|
|
||||||
));
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
|
||||||
|
|
||||||
$bookmark->setSticky(!$bookmark->isSticky());
|
|
||||||
|
|
||||||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
|
||||||
$data = $formatter->format($bookmark);
|
|
||||||
$this->executePageHooks('save_link', $data);
|
|
||||||
$bookmark->fromArray($data);
|
|
||||||
|
|
||||||
$this->container->bookmarkService->set($bookmark);
|
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function used to display the shaare form whether it's a new or existing bookmark.
|
|
||||||
*
|
|
||||||
* @param array $link data used in template, either from parameters or from the data store
|
|
||||||
*/
|
|
||||||
protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
|
|
||||||
{
|
|
||||||
$tags = $this->container->bookmarkService->bookmarksCountPerTag();
|
|
||||||
if ($this->container->conf->get('formatter') === 'markdown') {
|
|
||||||
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = escape([
|
|
||||||
'link' => $link,
|
|
||||||
'link_is_new' => $isNew,
|
|
||||||
'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
|
|
||||||
'source' => $request->getParam('source') ?? '',
|
|
||||||
'tags' => $tags,
|
|
||||||
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
|
||||||
|
|
||||||
foreach ($data as $key => $value) {
|
|
||||||
$this->assignView($key, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
$editLabel = false === $isNew ? t('Edit') .' ' : '';
|
|
||||||
$this->assignView(
|
|
||||||
'pagetitle',
|
|
||||||
$editLabel . t('Shaare') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
|
||||||
);
|
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::EDIT_LINK));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,9 +24,15 @@ public function index(Request $request, Response $response): Response
|
||||||
$fromTag = $request->getParam('fromtag') ?? '';
|
$fromTag = $request->getParam('fromtag') ?? '';
|
||||||
|
|
||||||
$this->assignView('fromtag', escape($fromTag));
|
$this->assignView('fromtag', escape($fromTag));
|
||||||
|
$separator = escape($this->container->conf->get('general.tags_separator', ' '));
|
||||||
|
if ($separator === ' ') {
|
||||||
|
$separator = ' ';
|
||||||
|
$this->assignView('tags_separator_desc', t('whitespace'));
|
||||||
|
}
|
||||||
|
$this->assignView('tags_separator', $separator);
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Manage tags') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::CHANGE_TAG));
|
return $response->write($this->render(TemplatePage::CHANGE_TAG));
|
||||||
|
@ -81,8 +87,35 @@ public function save(Request $request, Response $response): Response
|
||||||
|
|
||||||
$this->saveSuccessMessage($alert);
|
$this->saveSuccessMessage($alert);
|
||||||
|
|
||||||
$redirect = true === $isDelete ? '/admin/tags' : '/?searchtags='. urlencode($toTag);
|
$redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag);
|
||||||
|
|
||||||
return $this->redirect($response, $redirect);
|
return $this->redirect($response, $redirect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/tags/change-separator - Change tag separator
|
||||||
|
*/
|
||||||
|
public function changeSeparator(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
$reservedCharacters = ['-', '.', '*'];
|
||||||
|
$newSeparator = $request->getParam('separator');
|
||||||
|
if ($newSeparator === null || mb_strlen($newSeparator) !== 1) {
|
||||||
|
$this->saveErrorMessage(t('Tags separator must be a single character.'));
|
||||||
|
} elseif (in_array($newSeparator, $reservedCharacters, true)) {
|
||||||
|
$reservedCharacters = implode(' ', array_map(function (string $character) {
|
||||||
|
return '<code>' . $character . '</code>';
|
||||||
|
}, $reservedCharacters));
|
||||||
|
$this->saveErrorMessage(
|
||||||
|
t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$this->container->conf->set('general.tags_separator', $newSeparator, true, true);
|
||||||
|
|
||||||
|
$this->saveSuccessMessage('Your tags separator setting has been updated!');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($response, '/admin/tags');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
29
application/front/controller/admin/MetadataController.php
Normal file
29
application/front/controller/admin/MetadataController.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to retrieve/update bookmark's metadata.
|
||||||
|
*/
|
||||||
|
class MetadataController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* GET /admin/metadata/{url} - Attempt to retrieve the bookmark title from provided URL.
|
||||||
|
*/
|
||||||
|
public function ajaxRetrieveTitle(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$url = $request->getParam('url');
|
||||||
|
|
||||||
|
// Only try to extract metadata from URL with HTTP(s) scheme
|
||||||
|
if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
|
||||||
|
return $response->withJson($this->container->metadataRetriever->retrieve($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->withJson([]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ public function __construct(ShaarliContainer $container)
|
||||||
|
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ public function change(Request $request, Response $response): Response
|
||||||
|
|
||||||
// Save new password
|
// Save new password
|
||||||
// Salt renders rainbow-tables attacks useless.
|
// Salt renders rainbow-tables attacks useless.
|
||||||
$this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
|
$this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand()));
|
||||||
$this->container->conf->set(
|
$this->container->conf->set(
|
||||||
'credentials.hash',
|
'credentials.hash',
|
||||||
sha1(
|
sha1(
|
||||||
|
|
|
@ -42,7 +42,7 @@ function ($a, $b) {
|
||||||
$this->assignView('disabledPlugins', $disabledPlugins);
|
$this->assignView('disabledPlugins', $disabledPlugins);
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Plugin Administration') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
|
return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
|
||||||
|
@ -64,7 +64,7 @@ public function save(Request $request, Response $response): Response
|
||||||
unset($parameters['parameters_form']);
|
unset($parameters['parameters_form']);
|
||||||
unset($parameters['token']);
|
unset($parameters['token']);
|
||||||
foreach ($parameters as $param => $value) {
|
foreach ($parameters as $param => $value) {
|
||||||
$this->container->conf->set('plugins.'. $param, escape($value));
|
$this->container->conf->set('plugins.' . $param, escape($value));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
|
$this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
|
||||||
|
|
101
application/front/controller/admin/ServerController.php
Normal file
101
application/front/controller/admin/ServerController.php
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Shaarli\Helper\ApplicationUtils;
|
||||||
|
use Shaarli\Helper\FileUtils;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slim controller used to handle Server administration page, and actions.
|
||||||
|
*/
|
||||||
|
class ServerController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/** @var string Cache type - main - by default pagecache/ and tmp/ */
|
||||||
|
protected const CACHE_MAIN = 'main';
|
||||||
|
|
||||||
|
/** @var string Cache type - thumbnails - by default cache/ */
|
||||||
|
protected const CACHE_THUMB = 'thumbnails';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/server - Display page Server administration
|
||||||
|
*/
|
||||||
|
public function index(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$releaseUrl = ApplicationUtils::$GITHUB_URL . '/releases/';
|
||||||
|
if ($this->container->conf->get('updates.check_updates', true)) {
|
||||||
|
$latestVersion = 'v' . ApplicationUtils::getVersion(
|
||||||
|
ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
|
||||||
|
);
|
||||||
|
$releaseUrl .= 'tag/' . $latestVersion;
|
||||||
|
} else {
|
||||||
|
$latestVersion = t('Check disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
|
||||||
|
$currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
|
||||||
|
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
|
||||||
|
|
||||||
|
$permissions = array_merge(
|
||||||
|
ApplicationUtils::checkResourcePermissions($this->container->conf),
|
||||||
|
ApplicationUtils::checkDatastoreMutex()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assignView('php_version', PHP_VERSION);
|
||||||
|
$this->assignView('php_eol', format_date($phpEol, false));
|
||||||
|
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
|
||||||
|
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
|
||||||
|
$this->assignView('permissions', $permissions);
|
||||||
|
$this->assignView('release_url', $releaseUrl);
|
||||||
|
$this->assignView('latest_version', $latestVersion);
|
||||||
|
$this->assignView('current_version', $currentVersion);
|
||||||
|
$this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
|
||||||
|
$this->assignView('index_url', index_url($this->container->environment));
|
||||||
|
$this->assignView('client_ip', client_ip_id($this->container->environment));
|
||||||
|
$this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
|
||||||
|
|
||||||
|
$this->assignView(
|
||||||
|
'pagetitle',
|
||||||
|
t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->write($this->render('server'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
|
||||||
|
*/
|
||||||
|
public function clearCache(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$exclude = ['.htaccess'];
|
||||||
|
|
||||||
|
if ($request->getQueryParam('type') === static::CACHE_THUMB) {
|
||||||
|
$folders = [$this->container->conf->get('resource.thumbnails_cache')];
|
||||||
|
|
||||||
|
$this->saveWarningMessage(
|
||||||
|
t('Thumbnails cache has been cleared.') . ' ' .
|
||||||
|
'<a href="' . $this->container->basePath . '/admin/thumbnails">' .
|
||||||
|
t('Please synchronize them.') .
|
||||||
|
'</a>'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$folders = [
|
||||||
|
$this->container->conf->get('resource.page_cache'),
|
||||||
|
$this->container->conf->get('resource.raintpl_tmp'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure that we don't delete root cache folder
|
||||||
|
$folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
|
||||||
|
foreach ($folders as $folder) {
|
||||||
|
FileUtils::clearFolder($folder, false, $exclude);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect($response, '/admin/server');
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,6 +45,4 @@ public function visibility(Request $request, Response $response, array $args): R
|
||||||
|
|
||||||
return $this->redirectFromReferer($request, $response, ['visibility']);
|
return $this->redirectFromReferer($request, $response, ['visibility']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
34
application/front/controller/admin/ShaareAddController.php
Normal file
34
application/front/controller/admin/ShaareAddController.php
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Shaarli\Formatter\BookmarkMarkdownFormatter;
|
||||||
|
use Shaarli\Render\TemplatePage;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
class ShaareAddController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* GET /admin/add-shaare - Displays the form used to create a new bookmark from an URL
|
||||||
|
*/
|
||||||
|
public function addShaare(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$tags = $this->container->bookmarkService->bookmarksCountPerTag();
|
||||||
|
if ($this->container->conf->get('formatter') === 'markdown') {
|
||||||
|
$tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assignView(
|
||||||
|
'pagetitle',
|
||||||
|
t('Shaare a new link') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
|
);
|
||||||
|
$this->assignView('tags', $tags);
|
||||||
|
$this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false));
|
||||||
|
$this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
|
||||||
|
|
||||||
|
return $response->write($this->render(TemplatePage::ADDLINK));
|
||||||
|
}
|
||||||
|
}
|
202
application/front/controller/admin/ShaareManageController.php
Normal file
202
application/front/controller/admin/ShaareManageController.php
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PostBookmarkController
|
||||||
|
*
|
||||||
|
* Slim controller used to handle Shaarli create or edit bookmarks.
|
||||||
|
*/
|
||||||
|
class ShaareManageController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare/delete - Delete one or multiple bookmarks (depending on `id` query parameter).
|
||||||
|
*/
|
||||||
|
public function deleteBookmark(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
$ids = escape(trim($request->getParam('id') ?? ''));
|
||||||
|
if (empty($ids) || strpos($ids, ' ') !== false) {
|
||||||
|
// multiple, space-separated ids provided
|
||||||
|
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
|
||||||
|
} else {
|
||||||
|
$ids = [$ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert at least one id is given
|
||||||
|
if (0 === count($ids)) {
|
||||||
|
$this->saveErrorMessage(t('Invalid bookmark ID provided.'));
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
||||||
|
$count = 0;
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
try {
|
||||||
|
$bookmark = $this->container->bookmarkService->get((int) $id);
|
||||||
|
} catch (BookmarkNotFoundException $e) {
|
||||||
|
$this->saveErrorMessage(sprintf(
|
||||||
|
t('Bookmark with identifier %s could not be found.'),
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $formatter->format($bookmark);
|
||||||
|
$this->executePageHooks('delete_link', $data);
|
||||||
|
$this->container->bookmarkService->remove($bookmark, false);
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
$this->container->bookmarkService->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are called from the bookmarklet, we must close the popup:
|
||||||
|
if ($request->getParam('source') === 'bookmarklet') {
|
||||||
|
return $response->write('<script>self.close();</script>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't redirect to permalink after deletion.
|
||||||
|
return $this->redirectFromReferer($request, $response, ['shaare/']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare/visibility
|
||||||
|
*
|
||||||
|
* Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
|
||||||
|
*/
|
||||||
|
public function changeVisibility(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
$ids = trim(escape($request->getParam('id') ?? ''));
|
||||||
|
if (empty($ids) || strpos($ids, ' ') !== false) {
|
||||||
|
// multiple, space-separated ids provided
|
||||||
|
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
|
||||||
|
} else {
|
||||||
|
// only a single id provided
|
||||||
|
$ids = [$ids];
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert at least one id is given
|
||||||
|
if (0 === count($ids)) {
|
||||||
|
$this->saveErrorMessage(t('Invalid bookmark ID provided.'));
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert that the visibility is valid
|
||||||
|
$visibility = $request->getParam('newVisibility');
|
||||||
|
if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
|
||||||
|
$this->saveErrorMessage(t('Invalid visibility provided.'));
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
|
||||||
|
} else {
|
||||||
|
$isPrivate = $visibility === 'private';
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
||||||
|
$count = 0;
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
try {
|
||||||
|
$bookmark = $this->container->bookmarkService->get((int) $id);
|
||||||
|
} catch (BookmarkNotFoundException $e) {
|
||||||
|
$this->saveErrorMessage(sprintf(
|
||||||
|
t('Bookmark with identifier %s could not be found.'),
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookmark->setPrivate($isPrivate);
|
||||||
|
|
||||||
|
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||||
|
$data = $formatter->format($bookmark);
|
||||||
|
$this->executePageHooks('save_link', $data);
|
||||||
|
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||||
|
|
||||||
|
$this->container->bookmarkService->set($bookmark, false);
|
||||||
|
++$count;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($count > 0) {
|
||||||
|
$this->container->bookmarkService->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
|
||||||
|
*/
|
||||||
|
public function pinBookmark(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
$id = $args['id'] ?? '';
|
||||||
|
try {
|
||||||
|
if (false === ctype_digit($id)) {
|
||||||
|
throw new BookmarkNotFoundException();
|
||||||
|
}
|
||||||
|
$bookmark = $this->container->bookmarkService->get((int) $id); // Read database
|
||||||
|
} catch (BookmarkNotFoundException $e) {
|
||||||
|
$this->saveErrorMessage(sprintf(
|
||||||
|
t('Bookmark with identifier %s could not be found.'),
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
||||||
|
|
||||||
|
$bookmark->setSticky(!$bookmark->isSticky());
|
||||||
|
|
||||||
|
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||||
|
$data = $formatter->format($bookmark);
|
||||||
|
$this->executePageHooks('save_link', $data);
|
||||||
|
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||||
|
|
||||||
|
$this->container->bookmarkService->set($bookmark);
|
||||||
|
|
||||||
|
return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
|
||||||
|
*/
|
||||||
|
public function sharePrivate(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
$hash = $args['hash'] ?? '';
|
||||||
|
$bookmark = $this->container->bookmarkService->findByHash($hash);
|
||||||
|
|
||||||
|
if ($bookmark->isPrivate() !== true) {
|
||||||
|
return $this->redirect($response, '/shaare/' . $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
|
||||||
|
$privateKey = bin2hex(random_bytes(16));
|
||||||
|
$bookmark->addAdditionalContentEntry('private_key', $privateKey);
|
||||||
|
$this->container->bookmarkService->set($bookmark);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirect(
|
||||||
|
$response,
|
||||||
|
'/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
274
application/front/controller/admin/ShaarePublishController.php
Normal file
274
application/front/controller/admin/ShaarePublishController.php
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Front\Controller\Admin;
|
||||||
|
|
||||||
|
use Shaarli\Bookmark\Bookmark;
|
||||||
|
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||||
|
use Shaarli\Formatter\BookmarkFormatter;
|
||||||
|
use Shaarli\Formatter\BookmarkMarkdownFormatter;
|
||||||
|
use Shaarli\Render\TemplatePage;
|
||||||
|
use Shaarli\Thumbnailer;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
use Slim\Http\Response;
|
||||||
|
|
||||||
|
class ShaarePublishController extends ShaarliAdminController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BookmarkFormatter[] Statically cached instances of formatters
|
||||||
|
*/
|
||||||
|
protected $formatters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array Statically cached bookmark's tags counts
|
||||||
|
*/
|
||||||
|
protected $tags;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare - Displays the bookmark form for creation.
|
||||||
|
* Note that if the URL is found in existing bookmarks, then it will be in edit mode.
|
||||||
|
*/
|
||||||
|
public function displayCreateForm(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$url = cleanup_url($request->getParam('post'));
|
||||||
|
$link = $this->buildLinkDataFromUrl($request, $url);
|
||||||
|
|
||||||
|
return $this->displayForm($link, $link['linkIsNew'], $request, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page.
|
||||||
|
*/
|
||||||
|
public function displayCreateBatchForms(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls')));
|
||||||
|
|
||||||
|
$links = [];
|
||||||
|
foreach ($urls as $url) {
|
||||||
|
if (empty($url)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$link = $this->buildLinkDataFromUrl($request, $url);
|
||||||
|
$data = $this->buildFormData($link, $link['linkIsNew'], $request);
|
||||||
|
$data['token'] = $this->container->sessionManager->generateToken();
|
||||||
|
$data['source'] = 'batch';
|
||||||
|
|
||||||
|
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
||||||
|
|
||||||
|
$links[] = $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assignView('links', $links);
|
||||||
|
$this->assignView('batch_mode', true);
|
||||||
|
$this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
|
||||||
|
|
||||||
|
return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
|
||||||
|
*/
|
||||||
|
public function displayEditForm(Request $request, Response $response, array $args): Response
|
||||||
|
{
|
||||||
|
$id = $args['id'] ?? '';
|
||||||
|
try {
|
||||||
|
if (false === ctype_digit($id)) {
|
||||||
|
throw new BookmarkNotFoundException();
|
||||||
|
}
|
||||||
|
$bookmark = $this->container->bookmarkService->get((int) $id); // Read database
|
||||||
|
} catch (BookmarkNotFoundException $e) {
|
||||||
|
$this->saveErrorMessage(sprintf(
|
||||||
|
t('Bookmark with identifier %s could not be found.'),
|
||||||
|
$id
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this->redirect($response, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatter = $this->getFormatter('raw');
|
||||||
|
$link = $formatter->format($bookmark);
|
||||||
|
|
||||||
|
return $this->displayForm($link, false, $request, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /admin/shaare
|
||||||
|
*/
|
||||||
|
public function save(Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$this->checkToken($request);
|
||||||
|
|
||||||
|
// lf_id should only be present if the link exists.
|
||||||
|
$id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
|
||||||
|
if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
|
||||||
|
// Edit
|
||||||
|
$bookmark = $this->container->bookmarkService->get($id);
|
||||||
|
} else {
|
||||||
|
// New link
|
||||||
|
$bookmark = new Bookmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
$bookmark->setTitle($request->getParam('lf_title'));
|
||||||
|
$bookmark->setDescription($request->getParam('lf_description'));
|
||||||
|
$bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
|
||||||
|
$bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
|
||||||
|
$bookmark->setTagsString(
|
||||||
|
$request->getParam('lf_tags'),
|
||||||
|
$this->container->conf->get('general.tags_separator', ' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
$this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
||||||
|
&& true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||||
|
&& $bookmark->shouldUpdateThumbnail()
|
||||||
|
) {
|
||||||
|
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
|
||||||
|
}
|
||||||
|
$this->container->bookmarkService->addOrSet($bookmark, false);
|
||||||
|
|
||||||
|
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||||
|
$formatter = $this->getFormatter('raw');
|
||||||
|
$data = $formatter->format($bookmark);
|
||||||
|
$this->executePageHooks('save_link', $data);
|
||||||
|
|
||||||
|
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||||
|
$this->container->bookmarkService->set($bookmark);
|
||||||
|
|
||||||
|
// If we are called from the bookmarklet, we must close the popup:
|
||||||
|
if ($request->getParam('source') === 'bookmarklet') {
|
||||||
|
return $response->write('<script>self.close();</script>');
|
||||||
|
} elseif ($request->getParam('source') === 'batch') {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($request->getParam('returnurl'))) {
|
||||||
|
$this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->redirectFromReferer(
|
||||||
|
$request,
|
||||||
|
$response,
|
||||||
|
['/admin/add-shaare', '/admin/shaare'],
|
||||||
|
['addlink', 'post', 'edit_link'],
|
||||||
|
$bookmark->getShortUrl()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function used to display the shaare form whether it's a new or existing bookmark.
|
||||||
|
*
|
||||||
|
* @param array $link data used in template, either from parameters or from the data store
|
||||||
|
*/
|
||||||
|
protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
|
||||||
|
{
|
||||||
|
$data = $this->buildFormData($link, $isNew, $request);
|
||||||
|
|
||||||
|
$this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
|
||||||
|
|
||||||
|
foreach ($data as $key => $value) {
|
||||||
|
$this->assignView($key, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
$editLabel = false === $isNew ? t('Edit') . ' ' : '';
|
||||||
|
$this->assignView(
|
||||||
|
'pagetitle',
|
||||||
|
$editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $response->write($this->render(TemplatePage::EDIT_LINK));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildLinkDataFromUrl(Request $request, string $url): array
|
||||||
|
{
|
||||||
|
// Check if URL is not already in database (in this case, we will edit the existing link)
|
||||||
|
$bookmark = $this->container->bookmarkService->findByUrl($url);
|
||||||
|
if (null === $bookmark) {
|
||||||
|
// Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
|
||||||
|
$title = $request->getParam('title');
|
||||||
|
$description = $request->getParam('description');
|
||||||
|
$tags = $request->getParam('tags');
|
||||||
|
if ($request->getParam('private') !== null) {
|
||||||
|
$private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
|
||||||
|
} else {
|
||||||
|
$private = $this->container->conf->get('privacy.default_private_links', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an HTTP(S) link, we try go get the page to extract
|
||||||
|
// the title (otherwise we will to straight to the edit form.)
|
||||||
|
if (
|
||||||
|
true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||||
|
&& empty($title)
|
||||||
|
&& strpos(get_url_scheme($url) ?: '', 'http') !== false
|
||||||
|
) {
|
||||||
|
$metadata = $this->container->metadataRetriever->retrieve($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($url)) {
|
||||||
|
$metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'title' => $title ?? $metadata['title'] ?? '',
|
||||||
|
'url' => $url ?? '',
|
||||||
|
'description' => $description ?? $metadata['description'] ?? '',
|
||||||
|
'tags' => $tags ?? $metadata['tags'] ?? '',
|
||||||
|
'private' => $private,
|
||||||
|
'linkIsNew' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatter = $this->getFormatter('raw');
|
||||||
|
$link = $formatter->format($bookmark);
|
||||||
|
$link['linkIsNew'] = false;
|
||||||
|
|
||||||
|
return $link;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFormData(array $link, bool $isNew, Request $request): array
|
||||||
|
{
|
||||||
|
$link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0
|
||||||
|
? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
|
||||||
|
: $link['tags']
|
||||||
|
;
|
||||||
|
|
||||||
|
return escape([
|
||||||
|
'link' => $link,
|
||||||
|
'link_is_new' => $isNew,
|
||||||
|
'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
|
||||||
|
'source' => $request->getParam('source') ?? '',
|
||||||
|
'tags' => $this->getTags(),
|
||||||
|
'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
|
||||||
|
'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
|
||||||
|
'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoize formatterFactory->getFormatter() calls.
|
||||||
|
*/
|
||||||
|
protected function getFormatter(string $type): BookmarkFormatter
|
||||||
|
{
|
||||||
|
if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) {
|
||||||
|
$this->formatters[$type] = $this->container->formatterFactory->getFormatter($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->formatters[$type];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoize bookmarkService->bookmarksCountPerTag() calls.
|
||||||
|
*/
|
||||||
|
protected function getTags(): array
|
||||||
|
{
|
||||||
|
if ($this->tags === null) {
|
||||||
|
$this->tags = $this->container->bookmarkService->bookmarksCountPerTag();
|
||||||
|
|
||||||
|
if ($this->container->conf->get('formatter') === 'markdown') {
|
||||||
|
$this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,7 @@ public function index(Request $request, Response $response): Response
|
||||||
$this->assignView('ids', $ids);
|
$this->assignView('ids', $ids);
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Thumbnails update') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::THUMBNAILS));
|
return $response->write($this->render(TemplatePage::THUMBNAILS));
|
||||||
|
|
|
@ -28,7 +28,7 @@ public function index(Request $request, Response $response): Response
|
||||||
$this->assignView($key, $value);
|
$this->assignView($key, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assignView('pagetitle', t('Tools') .' - '. $this->container->conf->get('general.title', 'Shaarli'));
|
$this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::TOOLS));
|
return $response->write($this->render(TemplatePage::TOOLS));
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@ public function index(Request $request, Response $response): Response
|
||||||
$formatter->addContextData('base_path', $this->container->basePath);
|
$formatter->addContextData('base_path', $this->container->basePath);
|
||||||
|
|
||||||
$searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
|
$searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
|
||||||
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));;
|
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
|
||||||
|
;
|
||||||
|
|
||||||
// Filter bookmarks according search parameters.
|
// Filter bookmarks according search parameters.
|
||||||
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
|
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
|
||||||
|
@ -95,6 +96,10 @@ public function index(Request $request, Response $response): Response
|
||||||
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
|
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||||
|
$searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
|
||||||
|
$searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
|
||||||
|
|
||||||
// Fill all template fields.
|
// Fill all template fields.
|
||||||
$data = array_merge(
|
$data = array_merge(
|
||||||
$this->initializeTemplateVars(),
|
$this->initializeTemplateVars(),
|
||||||
|
@ -106,7 +111,7 @@ public function index(Request $request, Response $response): Response
|
||||||
'result_count' => count($linksToDisplay),
|
'result_count' => count($linksToDisplay),
|
||||||
'search_term' => escape($searchTerm),
|
'search_term' => escape($searchTerm),
|
||||||
'search_tags' => escape($searchTags),
|
'search_tags' => escape($searchTags),
|
||||||
'search_tags_url' => array_map('urlencode', explode(' ', $searchTags)),
|
'search_tags_url' => $searchTagsUrlEncoded,
|
||||||
'visibility' => $visibility,
|
'visibility' => $visibility,
|
||||||
'links' => $linkDisp,
|
'links' => $linkDisp,
|
||||||
]
|
]
|
||||||
|
@ -119,8 +124,9 @@ public function index(Request $request, Response $response): Response
|
||||||
return '[' . $tag . ']';
|
return '[' . $tag . ']';
|
||||||
};
|
};
|
||||||
$data['pagetitle'] .= ! empty($searchTags)
|
$data['pagetitle'] .= ! empty($searchTags)
|
||||||
? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchTags))) . ' '
|
? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
|
||||||
: '';
|
: ''
|
||||||
|
;
|
||||||
$data['pagetitle'] .= '- ';
|
$data['pagetitle'] .= '- ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,8 +143,10 @@ public function index(Request $request, Response $response): Response
|
||||||
*/
|
*/
|
||||||
public function permalink(Request $request, Response $response, array $args): Response
|
public function permalink(Request $request, Response $response, array $args): Response
|
||||||
{
|
{
|
||||||
|
$privateKey = $request->getParam('key');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$bookmark = $this->container->bookmarkService->findByHash($args['hash']);
|
$bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
|
||||||
} catch (BookmarkNotFoundException $e) {
|
} catch (BookmarkNotFoundException $e) {
|
||||||
$this->assignView('error_message', $e->getMessage());
|
$this->assignView('error_message', $e->getMessage());
|
||||||
|
|
||||||
|
@ -153,7 +161,7 @@ public function permalink(Request $request, Response $response, array $args): Re
|
||||||
$data = array_merge(
|
$data = array_merge(
|
||||||
$this->initializeTemplateVars(),
|
$this->initializeTemplateVars(),
|
||||||
[
|
[
|
||||||
'pagetitle' => $bookmark->getTitle() .' - '. $this->container->conf->get('general.title', 'Shaarli'),
|
'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'),
|
||||||
'links' => [$formatter->format($bookmark)],
|
'links' => [$formatter->format($bookmark)],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -169,19 +177,25 @@ public function permalink(Request $request, Response $response, array $args): Re
|
||||||
*/
|
*/
|
||||||
protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
|
protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
|
||||||
{
|
{
|
||||||
// Logged in, thumbnails enabled, not a note, is HTTP
|
if (false === $this->container->loginManager->isLoggedIn()) {
|
||||||
// and (never retrieved yet or no valid cache file)
|
return false;
|
||||||
if ($this->container->loginManager->isLoggedIn()
|
}
|
||||||
&& $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
|
||||||
&& false !== $bookmark->getThumbnail()
|
|
||||||
&& !$bookmark->isNote()
|
|
||||||
&& (null === $bookmark->getThumbnail() || !is_file($bookmark->getThumbnail()))
|
|
||||||
&& startsWith(strtolower($bookmark->getUrl()), 'http')
|
|
||||||
) {
|
|
||||||
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
|
|
||||||
$this->container->bookmarkService->set($bookmark, $writeDatastore);
|
|
||||||
|
|
||||||
return true;
|
// If thumbnail should be updated, we reset it to null
|
||||||
|
if ($bookmark->shouldUpdateThumbnail()) {
|
||||||
|
$bookmark->setThumbnail(null);
|
||||||
|
|
||||||
|
// Requires an update, not async retrieval, thumbnails enabled
|
||||||
|
if (
|
||||||
|
$bookmark->shouldUpdateThumbnail()
|
||||||
|
&& true !== $this->container->conf->get('general.enable_async_metadata', true)
|
||||||
|
&& $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
|
||||||
|
) {
|
||||||
|
$bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
|
||||||
|
$this->container->bookmarkService->set($bookmark, $writeDatastore);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -198,6 +212,7 @@ protected function initializeTemplateVars(): array
|
||||||
'page_max' => '',
|
'page_max' => '',
|
||||||
'search_tags' => '',
|
'search_tags' => '',
|
||||||
'result_count' => '',
|
'result_count' => '',
|
||||||
|
'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
namespace Shaarli\Front\Controller\Visitor;
|
namespace Shaarli\Front\Controller\Visitor;
|
||||||
|
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use DateTimeImmutable;
|
|
||||||
use Shaarli\Bookmark\Bookmark;
|
use Shaarli\Bookmark\Bookmark;
|
||||||
|
use Shaarli\Helper\DailyPageHelper;
|
||||||
use Shaarli\Render\TemplatePage;
|
use Shaarli\Render\TemplatePage;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
use Slim\Http\Response;
|
use Slim\Http\Response;
|
||||||
|
@ -26,32 +26,20 @@ class DailyController extends ShaarliVisitorController
|
||||||
*/
|
*/
|
||||||
public function index(Request $request, Response $response): Response
|
public function index(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$day = $request->getQueryParam('day') ?? date('Ymd');
|
$type = DailyPageHelper::extractRequestedType($request);
|
||||||
|
$format = DailyPageHelper::getFormatByType($type);
|
||||||
|
$latestBookmark = $this->container->bookmarkService->getLatest();
|
||||||
|
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
|
||||||
|
$start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
|
||||||
|
$end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
|
||||||
|
$dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
|
||||||
|
|
||||||
$availableDates = $this->container->bookmarkService->days();
|
$linksToDisplay = $this->container->bookmarkService->findByDate(
|
||||||
$nbAvailableDates = count($availableDates);
|
$start,
|
||||||
$index = array_search($day, $availableDates);
|
$end,
|
||||||
|
$previousDay,
|
||||||
if ($index === false) {
|
$nextDay
|
||||||
// no bookmarks for day, but at least one day with bookmarks
|
);
|
||||||
$day = $availableDates[$nbAvailableDates - 1] ?? $day;
|
|
||||||
$previousDay = $availableDates[$nbAvailableDates - 2] ?? '';
|
|
||||||
} else {
|
|
||||||
$previousDay = $availableDates[$index - 1] ?? '';
|
|
||||||
$nextDay = $availableDates[$index + 1] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($day === date('Ymd')) {
|
|
||||||
$this->assignView('dayDesc', t('Today'));
|
|
||||||
} elseif ($day === date('Ymd', strtotime('-1 days'))) {
|
|
||||||
$this->assignView('dayDesc', t('Yesterday'));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$linksToDisplay = $this->container->bookmarkService->filterDay($day);
|
|
||||||
} catch (\Exception $exc) {
|
|
||||||
$linksToDisplay = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$formatter = $this->container->formatterFactory->getFormatter();
|
$formatter = $this->container->formatterFactory->getFormatter();
|
||||||
$formatter->addContextData('base_path', $this->container->basePath);
|
$formatter->addContextData('base_path', $this->container->basePath);
|
||||||
|
@ -63,13 +51,15 @@ public function index(Request $request, Response $response): Response
|
||||||
$linksToDisplay[$key]['description'] = $bookmark->getDescription();
|
$linksToDisplay[$key]['description'] = $bookmark->getDescription();
|
||||||
}
|
}
|
||||||
|
|
||||||
$dayDate = DateTime::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
|
|
||||||
$data = [
|
$data = [
|
||||||
'linksToDisplay' => $linksToDisplay,
|
'linksToDisplay' => $linksToDisplay,
|
||||||
'day' => $dayDate->getTimestamp(),
|
'dayDate' => $start,
|
||||||
'dayDate' => $dayDate,
|
'day' => $start->getTimestamp(),
|
||||||
'previousday' => $previousDay ?? '',
|
'previousday' => $previousDay ? $previousDay->format($format) : '',
|
||||||
'nextday' => $nextDay ?? '',
|
'nextday' => $nextDay ? $nextDay->format($format) : '',
|
||||||
|
'dayDesc' => $dailyDesc,
|
||||||
|
'type' => $type,
|
||||||
|
'localizedType' => $this->translateType($type),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Hooks are called before column construction so that plugins don't have to deal with columns.
|
// Hooks are called before column construction so that plugins don't have to deal with columns.
|
||||||
|
@ -82,7 +72,7 @@ public function index(Request $request, Response $response): Response
|
||||||
$mainTitle = $this->container->conf->get('general.title', 'Shaarli');
|
$mainTitle = $this->container->conf->get('general.title', 'Shaarli');
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Daily') .' - '. format_date($dayDate, false) . ' - ' . $mainTitle
|
$data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
|
||||||
);
|
);
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::DAILY));
|
return $response->write($this->render(TemplatePage::DAILY));
|
||||||
|
@ -96,9 +86,11 @@ public function index(Request $request, Response $response): Response
|
||||||
public function rss(Request $request, Response $response): Response
|
public function rss(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
|
||||||
|
$type = DailyPageHelper::extractRequestedType($request);
|
||||||
|
$cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
|
||||||
|
|
||||||
$pageUrl = page_url($this->container->environment);
|
$pageUrl = page_url($this->container->environment);
|
||||||
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
|
$cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
|
||||||
|
|
||||||
$cached = $cache->cachedVersion();
|
$cached = $cache->cachedVersion();
|
||||||
if (!empty($cached)) {
|
if (!empty($cached)) {
|
||||||
|
@ -106,11 +98,13 @@ public function rss(Request $request, Response $response): Response
|
||||||
}
|
}
|
||||||
|
|
||||||
$days = [];
|
$days = [];
|
||||||
|
$format = DailyPageHelper::getFormatByType($type);
|
||||||
|
$length = DailyPageHelper::getRssLengthByType($type);
|
||||||
foreach ($this->container->bookmarkService->search() as $bookmark) {
|
foreach ($this->container->bookmarkService->search() as $bookmark) {
|
||||||
$day = $bookmark->getCreated()->format('Ymd');
|
$day = $bookmark->getCreated()->format($format);
|
||||||
|
|
||||||
// Stop iterating after DAILY_RSS_NB_DAYS entries
|
// Stop iterating after DAILY_RSS_NB_DAYS entries
|
||||||
if (count($days) === static::$DAILY_RSS_NB_DAYS && !isset($days[$day])) {
|
if (count($days) === $length && !isset($days[$day])) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +121,19 @@ public function rss(Request $request, Response $response): Response
|
||||||
|
|
||||||
/** @var Bookmark[] $bookmarks */
|
/** @var Bookmark[] $bookmarks */
|
||||||
foreach ($days as $day => $bookmarks) {
|
foreach ($days as $day => $bookmarks) {
|
||||||
$dayDatetime = DateTimeImmutable::createFromFormat(Bookmark::LINK_DATE_FORMAT, $day.'_000000');
|
$dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
|
||||||
|
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
|
||||||
|
|
||||||
|
// We only want the RSS entry to be published when the period is over.
|
||||||
|
if (new DateTime() < $endDateTime) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$dataPerDay[$day] = [
|
$dataPerDay[$day] = [
|
||||||
'date' => $dayDatetime,
|
'date' => $endDateTime,
|
||||||
'date_rss' => $dayDatetime->format(DateTime::RSS),
|
'date_rss' => $endDateTime->format(DateTime::RSS),
|
||||||
'date_human' => format_date($dayDatetime, false, true),
|
'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
|
||||||
'absolute_url' => $indexUrl . 'daily?day=' . $day,
|
'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
|
||||||
'links' => [],
|
'links' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -141,16 +142,20 @@ public function rss(Request $request, Response $response): Response
|
||||||
|
|
||||||
// Make permalink URL absolute
|
// Make permalink URL absolute
|
||||||
if ($bookmark->isNote()) {
|
if ($bookmark->isNote()) {
|
||||||
$dataPerDay[$day]['links'][$key]['url'] = $indexUrl . $bookmark->getUrl();
|
$dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
|
$this->assignAllView([
|
||||||
$this->assignView('index_url', $indexUrl);
|
'title' => $this->container->conf->get('general.title', 'Shaarli'),
|
||||||
$this->assignView('page_url', $pageUrl);
|
'index_url' => $indexUrl,
|
||||||
$this->assignView('hide_timestamps', $this->container->conf->get('privacy.hide_timestamps', false));
|
'page_url' => $pageUrl,
|
||||||
$this->assignView('days', $dataPerDay);
|
'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
|
||||||
|
'days' => $dataPerDay,
|
||||||
|
'type' => $type,
|
||||||
|
'localizedType' => $this->translateType($type),
|
||||||
|
]);
|
||||||
|
|
||||||
$rssContent = $this->render(TemplatePage::DAILY_RSS);
|
$rssContent = $this->render(TemplatePage::DAILY_RSS);
|
||||||
|
|
||||||
|
@ -189,4 +194,13 @@ protected function calculateColumns(array $links): array
|
||||||
|
|
||||||
return $columns;
|
return $columns;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function translateType($type): string
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
t('day') => t('Daily'),
|
||||||
|
t('week') => t('Weekly'),
|
||||||
|
t('month') => t('Monthly'),
|
||||||
|
][t($type)] ?? t('Daily');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,12 +26,15 @@ public function __invoke(Request $request, Response $response, \Throwable $throw
|
||||||
$response = $response->withStatus($throwable->getCode());
|
$response = $response->withStatus($throwable->getCode());
|
||||||
} else {
|
} else {
|
||||||
// Internal error (any other Throwable)
|
// Internal error (any other Throwable)
|
||||||
if ($this->container->conf->get('dev.debug', false)) {
|
if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) {
|
||||||
$this->assignView('message', $throwable->getMessage());
|
$this->assignView('message', t('Error: ') . $throwable->getMessage());
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'stacktrace',
|
'text',
|
||||||
nl2br(get_class($throwable) .': '. PHP_EOL . $throwable->getTraceAsString())
|
'<a href="https://github.com/shaarli/Shaarli/issues/new">'
|
||||||
|
. t('Please report it on Github.')
|
||||||
|
. '</a>'
|
||||||
);
|
);
|
||||||
|
$this->assignView('stacktrace', exception2text($throwable));
|
||||||
} else {
|
} else {
|
||||||
$this->assignView('message', t('An unexpected error occurred.'));
|
$this->assignView('message', t('An unexpected error occurred.'));
|
||||||
}
|
}
|
||||||
|
@ -39,7 +42,6 @@ public function __invoke(Request $request, Response $response, \Throwable $throw
|
||||||
$response = $response->withStatus(500);
|
$response = $response->withStatus(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $response->write($this->render('error'));
|
return $response->write($this->render('error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public function rss(Request $request, Response $response): Response
|
||||||
|
|
||||||
protected function processRequest(string $feedType, Request $request, Response $response): Response
|
protected function processRequest(string $feedType, Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$response = $response->withHeader('Content-Type', 'application/'. $feedType .'+xml; charset=utf-8');
|
$response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8');
|
||||||
|
|
||||||
$pageUrl = page_url($this->container->environment);
|
$pageUrl = page_url($this->container->environment);
|
||||||
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
|
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
|
||||||
|
|
|
@ -4,10 +4,10 @@
|
||||||
|
|
||||||
namespace Shaarli\Front\Controller\Visitor;
|
namespace Shaarli\Front\Controller\Visitor;
|
||||||
|
|
||||||
use Shaarli\ApplicationUtils;
|
|
||||||
use Shaarli\Container\ShaarliContainer;
|
use Shaarli\Container\ShaarliContainer;
|
||||||
use Shaarli\Front\Exception\AlreadyInstalledException;
|
use Shaarli\Front\Exception\AlreadyInstalledException;
|
||||||
use Shaarli\Front\Exception\ResourcePermissionException;
|
use Shaarli\Front\Exception\ResourcePermissionException;
|
||||||
|
use Shaarli\Helper\ApplicationUtils;
|
||||||
use Shaarli\Languages;
|
use Shaarli\Languages;
|
||||||
use Shaarli\Security\SessionManager;
|
use Shaarli\Security\SessionManager;
|
||||||
use Slim\Http\Request;
|
use Slim\Http\Request;
|
||||||
|
@ -39,7 +39,8 @@ public function index(Request $request, Response $response): Response
|
||||||
// Before installation, we'll make sure that permissions are set properly, and sessions are working.
|
// Before installation, we'll make sure that permissions are set properly, and sessions are working.
|
||||||
$this->checkPermissions();
|
$this->checkPermissions();
|
||||||
|
|
||||||
if (static::SESSION_TEST_VALUE
|
if (
|
||||||
|
static::SESSION_TEST_VALUE
|
||||||
!== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
|
!== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
|
||||||
) {
|
) {
|
||||||
$this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
|
$this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
|
||||||
|
@ -53,6 +54,21 @@ public function index(Request $request, Response $response): Response
|
||||||
$this->assignView('cities', $cities);
|
$this->assignView('cities', $cities);
|
||||||
$this->assignView('languages', Languages::getAvailableLanguages());
|
$this->assignView('languages', Languages::getAvailableLanguages());
|
||||||
|
|
||||||
|
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
|
||||||
|
|
||||||
|
$permissions = array_merge(
|
||||||
|
ApplicationUtils::checkResourcePermissions($this->container->conf),
|
||||||
|
ApplicationUtils::checkDatastoreMutex()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assignView('php_version', PHP_VERSION);
|
||||||
|
$this->assignView('php_eol', format_date($phpEol, false));
|
||||||
|
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
|
||||||
|
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
|
||||||
|
$this->assignView('permissions', $permissions);
|
||||||
|
|
||||||
|
$this->assignView('pagetitle', t('Install Shaarli'));
|
||||||
|
|
||||||
return $response->write($this->render('install'));
|
return $response->write($this->render('install'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,17 +81,18 @@ public function sessionTest(Request $request, Response $response): Response
|
||||||
// This part makes sure sessions works correctly.
|
// This part makes sure sessions works correctly.
|
||||||
// (Because on some hosts, session.save_path may not be set correctly,
|
// (Because on some hosts, session.save_path may not be set correctly,
|
||||||
// or we may not have write access to it.)
|
// or we may not have write access to it.)
|
||||||
if (static::SESSION_TEST_VALUE
|
if (
|
||||||
|
static::SESSION_TEST_VALUE
|
||||||
!== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
|
!== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
|
||||||
) {
|
) {
|
||||||
// Step 2: Check if data in session is correct.
|
// Step 2: Check if data in session is correct.
|
||||||
$msg = t(
|
$msg = t(
|
||||||
'<pre>Sessions do not seem to work correctly on your server.<br>'.
|
'<pre>Sessions do not seem to work correctly on your server.<br>' .
|
||||||
'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
|
'Make sure the variable "session.save_path" is set correctly in your PHP config, ' .
|
||||||
'and that you have write access to it.<br>'.
|
'and that you have write access to it.<br>' .
|
||||||
'It currently points to %s.<br>'.
|
'It currently points to %s.<br>' .
|
||||||
'On some browsers, accessing your server via a hostname like \'localhost\' '.
|
'On some browsers, accessing your server via a hostname like \'localhost\' ' .
|
||||||
'or any custom hostname without a dot causes cookie storage to fail. '.
|
'or any custom hostname without a dot causes cookie storage to fail. ' .
|
||||||
'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
|
'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
|
||||||
);
|
);
|
||||||
$msg = sprintf($msg, $this->container->sessionManager->getSavePath());
|
$msg = sprintf($msg, $this->container->sessionManager->getSavePath());
|
||||||
|
@ -94,7 +111,8 @@ public function sessionTest(Request $request, Response $response): Response
|
||||||
public function save(Request $request, Response $response): Response
|
public function save(Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
$timezone = 'UTC';
|
$timezone = 'UTC';
|
||||||
if (!empty($request->getParam('continent'))
|
if (
|
||||||
|
!empty($request->getParam('continent'))
|
||||||
&& !empty($request->getParam('city'))
|
&& !empty($request->getParam('city'))
|
||||||
&& isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
|
&& isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
|
||||||
) {
|
) {
|
||||||
|
@ -104,7 +122,7 @@ public function save(Request $request, Response $response): Response
|
||||||
|
|
||||||
$login = $request->getParam('setlogin');
|
$login = $request->getParam('setlogin');
|
||||||
$this->container->conf->set('credentials.login', $login);
|
$this->container->conf->set('credentials.login', $login);
|
||||||
$salt = sha1(uniqid('', true) .'_'. mt_rand());
|
$salt = sha1(uniqid('', true) . '_' . mt_rand());
|
||||||
$this->container->conf->set('credentials.salt', $salt);
|
$this->container->conf->set('credentials.salt', $salt);
|
||||||
$this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
|
$this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
|
||||||
|
|
||||||
|
@ -113,7 +131,7 @@ public function save(Request $request, Response $response): Response
|
||||||
} else {
|
} else {
|
||||||
$this->container->conf->set(
|
$this->container->conf->set(
|
||||||
'general.title',
|
'general.title',
|
||||||
'Shared bookmarks on '.escape(index_url($this->container->environment))
|
'Shared bookmarks on ' . escape(index_url($this->container->environment))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +168,7 @@ public function save(Request $request, Response $response): Response
|
||||||
protected function checkPermissions(): bool
|
protected function checkPermissions(): bool
|
||||||
{
|
{
|
||||||
// Ensure Shaarli has proper access to its resources
|
// Ensure Shaarli has proper access to its resources
|
||||||
$errors = ApplicationUtils::checkResourcePermissions($this->container->conf);
|
$errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
|
||||||
if (empty($errors)) {
|
if (empty($errors)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public function index(Request $request, Response $response): Response
|
||||||
$this
|
$this
|
||||||
->assignView('returnurl', escape($returnUrl))
|
->assignView('returnurl', escape($returnUrl))
|
||||||
->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
|
->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
|
||||||
->assignView('pagetitle', t('Login') .' - '. $this->container->conf->get('general.title', 'Shaarli'))
|
->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'))
|
||||||
;
|
;
|
||||||
|
|
||||||
return $response->write($this->render(TemplatePage::LOGIN));
|
return $response->write($this->render(TemplatePage::LOGIN));
|
||||||
|
@ -64,8 +64,8 @@ public function login(Request $request, Response $response): Response
|
||||||
return $this->redirect($response, '/');
|
return $this->redirect($response, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->container->loginManager->checkCredentials(
|
if (
|
||||||
$this->container->environment['REMOTE_ADDR'],
|
!$this->container->loginManager->checkCredentials(
|
||||||
client_ip_id($this->container->environment),
|
client_ip_id($this->container->environment),
|
||||||
$request->getParam('login'),
|
$request->getParam('login'),
|
||||||
$request->getParam('password')
|
$request->getParam('password')
|
||||||
|
@ -102,7 +102,8 @@ public function login(Request $request, Response $response): Response
|
||||||
*/
|
*/
|
||||||
protected function checkLoginState(): bool
|
protected function checkLoginState(): bool
|
||||||
{
|
{
|
||||||
if ($this->container->loginManager->isLoggedIn()
|
if (
|
||||||
|
$this->container->loginManager->isLoggedIn()
|
||||||
|| $this->container->conf->get('security.open_shaarli', false)
|
|| $this->container->conf->get('security.open_shaarli', false)
|
||||||
) {
|
) {
|
||||||
throw new CantLoginException();
|
throw new CantLoginException();
|
||||||
|
|
|
@ -26,7 +26,7 @@ public function index(Request $request, Response $response): Response
|
||||||
|
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
t('Picture wall') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Optionally filter the results:
|
// Optionally filter the results:
|
||||||
|
|
|
@ -144,7 +144,8 @@ protected function redirectFromReferer(
|
||||||
if (null !== $referer) {
|
if (null !== $referer) {
|
||||||
$currentUrl = parse_url($referer);
|
$currentUrl = parse_url($referer);
|
||||||
// If the referer is not related to Shaarli instance, redirect to default
|
// If the referer is not related to Shaarli instance, redirect to default
|
||||||
if (isset($currentUrl['host'])
|
if (
|
||||||
|
isset($currentUrl['host'])
|
||||||
&& strpos(index_url($this->container->environment), $currentUrl['host']) === false
|
&& strpos(index_url($this->container->environment), $currentUrl['host']) === false
|
||||||
) {
|
) {
|
||||||
return $response->withRedirect($defaultPath);
|
return $response->withRedirect($defaultPath);
|
||||||
|
@ -173,7 +174,7 @@ protected function redirectFromReferer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$queryString = count($params) > 0 ? '?'. http_build_query($params) : '';
|
$queryString = count($params) > 0 ? '?' . http_build_query($params) : '';
|
||||||
$anchor = $anchor ? '#' . $anchor : '';
|
$anchor = $anchor ? '#' . $anchor : '';
|
||||||
|
|
||||||
return $response->withRedirect($path . $queryString . $anchor);
|
return $response->withRedirect($path . $queryString . $anchor);
|
||||||
|
|
|
@ -47,13 +47,14 @@ public function list(Request $request, Response $response): Response
|
||||||
*/
|
*/
|
||||||
protected function processRequest(string $type, Request $request, Response $response): Response
|
protected function processRequest(string $type, Request $request, Response $response): Response
|
||||||
{
|
{
|
||||||
|
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||||
if ($this->container->loginManager->isLoggedIn() === true) {
|
if ($this->container->loginManager->isLoggedIn() === true) {
|
||||||
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
|
$visibility = $this->container->sessionManager->getSessionParameter('visibility');
|
||||||
}
|
}
|
||||||
|
|
||||||
$sort = $request->getQueryParam('sort');
|
$sort = $request->getQueryParam('sort');
|
||||||
$searchTags = $request->getQueryParam('searchtags');
|
$searchTags = $request->getQueryParam('searchtags');
|
||||||
$filteringTags = $searchTags !== null ? explode(' ', $searchTags) : [];
|
$filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : [];
|
||||||
|
|
||||||
$tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
|
$tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
|
||||||
|
|
||||||
|
@ -71,8 +72,9 @@ protected function processRequest(string $type, Request $request, Response $resp
|
||||||
$tagsUrl[escape($tag)] = urlencode((string) $tag);
|
$tagsUrl[escape($tag)] = urlencode((string) $tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
$searchTags = implode(' ', escape($filteringTags));
|
$searchTags = tags_array2str($filteringTags, $tagsSeparator);
|
||||||
$searchTagsUrl = urlencode(implode(' ', $filteringTags));
|
$searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
|
||||||
|
$searchTagsUrl = urlencode($searchTags);
|
||||||
$data = [
|
$data = [
|
||||||
'search_tags' => escape($searchTags),
|
'search_tags' => escape($searchTags),
|
||||||
'search_tags_url' => $searchTagsUrl,
|
'search_tags_url' => $searchTagsUrl,
|
||||||
|
@ -82,10 +84,10 @@ protected function processRequest(string $type, Request $request, Response $resp
|
||||||
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
|
$this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
|
||||||
$this->assignAllView($data);
|
$this->assignAllView($data);
|
||||||
|
|
||||||
$searchTags = !empty($searchTags) ? $searchTags .' - ' : '';
|
$searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : '';
|
||||||
$this->assignView(
|
$this->assignView(
|
||||||
'pagetitle',
|
'pagetitle',
|
||||||
$searchTags . t('Tag '. $type) .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
$searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
|
||||||
);
|
);
|
||||||
|
|
||||||
return $response->write($this->render('tag.' . $type));
|
return $response->write($this->render('tag.' . $type));
|
||||||
|
|
|
@ -27,7 +27,7 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
||||||
// In case browser does not send HTTP_REFERER, we search a single tag
|
// In case browser does not send HTTP_REFERER, we search a single tag
|
||||||
if (null === $referer) {
|
if (null === $referer) {
|
||||||
if (null !== $newTag) {
|
if (null !== $newTag) {
|
||||||
return $this->redirect($response, '/?searchtags='. urlencode($newTag));
|
return $this->redirect($response, '/?searchtags=' . urlencode($newTag));
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirect($response, '/');
|
return $this->redirect($response, '/');
|
||||||
|
@ -37,7 +37,7 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
||||||
parse_str($currentUrl['query'] ?? '', $params);
|
parse_str($currentUrl['query'] ?? '', $params);
|
||||||
|
|
||||||
if (null === $newTag) {
|
if (null === $newTag) {
|
||||||
return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
|
return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent redirection loop
|
// Prevent redirection loop
|
||||||
|
@ -45,9 +45,10 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
||||||
unset($params['addtag']);
|
unset($params['addtag']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||||
// Check if this tag is already in the search query and ignore it if it is.
|
// Check if this tag is already in the search query and ignore it if it is.
|
||||||
// Each tag is always separated by a space
|
// Each tag is always separated by a space
|
||||||
$currentTags = isset($params['searchtags']) ? explode(' ', $params['searchtags']) : [];
|
$currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
|
||||||
|
|
||||||
$addtag = true;
|
$addtag = true;
|
||||||
foreach ($currentTags as $value) {
|
foreach ($currentTags as $value) {
|
||||||
|
@ -62,12 +63,12 @@ public function addTag(Request $request, Response $response, array $args): Respo
|
||||||
$currentTags[] = trim($newTag);
|
$currentTags[] = trim($newTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
$params['searchtags'] = trim(implode(' ', $currentTags));
|
$params['searchtags'] = tags_array2str($currentTags, $tagsSeparator);
|
||||||
|
|
||||||
// We also remove page (keeping the same page has no sense, since the results are different)
|
// We also remove page (keeping the same page has no sense, since the results are different)
|
||||||
unset($params['page']);
|
unset($params['page']);
|
||||||
|
|
||||||
return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
|
return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,7 +90,7 @@ public function removeTag(Request $request, Response $response, array $args): Re
|
||||||
parse_str($currentUrl['query'] ?? '', $params);
|
parse_str($currentUrl['query'] ?? '', $params);
|
||||||
|
|
||||||
if (null === $tagToRemove) {
|
if (null === $tagToRemove) {
|
||||||
return $response->withRedirect(($currentUrl['path'] ?? './') .'?'. http_build_query($params));
|
return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent redirection loop
|
// Prevent redirection loop
|
||||||
|
@ -98,10 +99,11 @@ public function removeTag(Request $request, Response $response, array $args): Re
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($params['searchtags'])) {
|
if (isset($params['searchtags'])) {
|
||||||
$tags = explode(' ', $params['searchtags']);
|
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
|
||||||
|
$tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
|
||||||
// Remove value from array $tags.
|
// Remove value from array $tags.
|
||||||
$tags = array_diff($tags, [$tagToRemove]);
|
$tags = array_diff($tags, [$tagToRemove]);
|
||||||
$params['searchtags'] = implode(' ', $tags);
|
$params['searchtags'] = tags_array2str($tags, $tagsSeparator);
|
||||||
|
|
||||||
if (empty($params['searchtags'])) {
|
if (empty($params['searchtags'])) {
|
||||||
unset($params['searchtags']);
|
unset($params['searchtags']);
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Shaarli;
|
|
||||||
|
namespace Shaarli\Helper;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use malkusch\lock\exception\LockAcquireException;
|
||||||
|
use malkusch\lock\mutex\FlockMutex;
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,8 +17,9 @@ class ApplicationUtils
|
||||||
*/
|
*/
|
||||||
public static $VERSION_FILE = 'shaarli_version.php';
|
public static $VERSION_FILE = 'shaarli_version.php';
|
||||||
|
|
||||||
private static $GIT_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
|
public static $GITHUB_URL = 'https://github.com/shaarli/Shaarli';
|
||||||
private static $GIT_BRANCHES = array('latest', 'stable');
|
public static $GIT_RAW_URL = 'https://raw.githubusercontent.com/shaarli/Shaarli';
|
||||||
|
public static $GIT_BRANCHES = ['latest', 'stable'];
|
||||||
private static $VERSION_START_TAG = '<?php /* ';
|
private static $VERSION_START_TAG = '<?php /* ';
|
||||||
private static $VERSION_END_TAG = ' */ ?>';
|
private static $VERSION_END_TAG = ' */ ?>';
|
||||||
|
|
||||||
|
@ -63,8 +67,8 @@ public static function getVersion($remote, $timeout = 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
return str_replace(
|
return str_replace(
|
||||||
array(self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL),
|
[self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL],
|
||||||
array('', '', ''),
|
['', '', ''],
|
||||||
$data
|
$data
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -125,7 +129,7 @@ public static function checkUpdate(
|
||||||
// Late Static Binding allows overriding within tests
|
// Late Static Binding allows overriding within tests
|
||||||
// See http://php.net/manual/en/language.oop5.late-static-bindings.php
|
// See http://php.net/manual/en/language.oop5.late-static-bindings.php
|
||||||
$latestVersion = static::getVersion(
|
$latestVersion = static::getVersion(
|
||||||
self::$GIT_URL . '/' . $branch . '/' . self::$VERSION_FILE
|
self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!$latestVersion) {
|
if (!$latestVersion) {
|
||||||
|
@ -171,35 +175,47 @@ public static function checkPHPVersion($minVersion, $curVersion)
|
||||||
/**
|
/**
|
||||||
* Checks Shaarli has the proper access permissions to its resources
|
* Checks Shaarli has the proper access permissions to its resources
|
||||||
*
|
*
|
||||||
* @param ConfigManager $conf Configuration Manager instance.
|
* @param ConfigManager $conf Configuration Manager instance.
|
||||||
|
* @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
|
||||||
|
* Currently we only need to be able to read the theme and write in raintpl cache.
|
||||||
*
|
*
|
||||||
* @return array A list of the detected configuration issues
|
* @return array A list of the detected configuration issues
|
||||||
*/
|
*/
|
||||||
public static function checkResourcePermissions($conf)
|
public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
|
||||||
{
|
{
|
||||||
$errors = array();
|
$errors = [];
|
||||||
$rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
|
$rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
|
||||||
|
|
||||||
// Check script and template directories are readable
|
// Check script and template directories are readable
|
||||||
foreach (array(
|
foreach (
|
||||||
'application',
|
[
|
||||||
'inc',
|
'application',
|
||||||
'plugins',
|
'inc',
|
||||||
$rainTplDir,
|
'plugins',
|
||||||
$rainTplDir . '/' . $conf->get('resource.theme'),
|
$rainTplDir,
|
||||||
) as $path) {
|
$rainTplDir . '/' . $conf->get('resource.theme'),
|
||||||
|
] as $path
|
||||||
|
) {
|
||||||
if (!is_readable(realpath($path))) {
|
if (!is_readable(realpath($path))) {
|
||||||
$errors[] = '"' . $path . '" ' . t('directory is not readable');
|
$errors[] = '"' . $path . '" ' . t('directory is not readable');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cache and data directories are readable and writable
|
// Check cache and data directories are readable and writable
|
||||||
foreach (array(
|
if ($minimalMode) {
|
||||||
$conf->get('resource.thumbnails_cache'),
|
$folders = [
|
||||||
$conf->get('resource.data_dir'),
|
$conf->get('resource.raintpl_tmp'),
|
||||||
$conf->get('resource.page_cache'),
|
];
|
||||||
$conf->get('resource.raintpl_tmp'),
|
} else {
|
||||||
) as $path) {
|
$folders = [
|
||||||
|
$conf->get('resource.thumbnails_cache'),
|
||||||
|
$conf->get('resource.data_dir'),
|
||||||
|
$conf->get('resource.page_cache'),
|
||||||
|
$conf->get('resource.raintpl_tmp'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($folders as $path) {
|
||||||
if (!is_readable(realpath($path))) {
|
if (!is_readable(realpath($path))) {
|
||||||
$errors[] = '"' . $path . '" ' . t('directory is not readable');
|
$errors[] = '"' . $path . '" ' . t('directory is not readable');
|
||||||
}
|
}
|
||||||
|
@ -208,14 +224,20 @@ public static function checkResourcePermissions($conf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($minimalMode) {
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
// Check configuration files are readable and writable
|
// Check configuration files are readable and writable
|
||||||
foreach (array(
|
foreach (
|
||||||
$conf->getConfigFileExt(),
|
[
|
||||||
$conf->get('resource.datastore'),
|
$conf->getConfigFileExt(),
|
||||||
$conf->get('resource.ban_file'),
|
$conf->get('resource.datastore'),
|
||||||
$conf->get('resource.log'),
|
$conf->get('resource.ban_file'),
|
||||||
$conf->get('resource.update_check'),
|
$conf->get('resource.log'),
|
||||||
) as $path) {
|
$conf->get('resource.update_check'),
|
||||||
|
] as $path
|
||||||
|
) {
|
||||||
if (!is_file(realpath($path))) {
|
if (!is_file(realpath($path))) {
|
||||||
# the file may not exist yet
|
# the file may not exist yet
|
||||||
continue;
|
continue;
|
||||||
|
@ -232,6 +254,20 @@ public static function checkResourcePermissions($conf)
|
||||||
return $errors;
|
return $errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function checkDatastoreMutex(): array
|
||||||
|
{
|
||||||
|
$mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2);
|
||||||
|
try {
|
||||||
|
$mutex->synchronized(function () {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} catch (LockAcquireException $e) {
|
||||||
|
$errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a salted hash representing the current Shaarli version.
|
* Returns a salted hash representing the current Shaarli version.
|
||||||
*
|
*
|
||||||
|
@ -246,4 +282,54 @@ public static function getVersionHash($currentVersion, $salt)
|
||||||
{
|
{
|
||||||
return hash_hmac('sha256', $currentVersion, $salt);
|
return hash_hmac('sha256', $currentVersion, $salt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of PHP extensions used by Shaarli.
|
||||||
|
*
|
||||||
|
* @return array[] List of extension with following keys:
|
||||||
|
* - name: extension name
|
||||||
|
* - required: whether the extension is required to use Shaarli
|
||||||
|
* - desc: short description of extension usage in Shaarli
|
||||||
|
* - loaded: whether the extension is properly loaded or not
|
||||||
|
*/
|
||||||
|
public static function getPhpExtensionsRequirement(): array
|
||||||
|
{
|
||||||
|
$extensions = [
|
||||||
|
['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
|
||||||
|
['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
|
||||||
|
['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
|
||||||
|
['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
|
||||||
|
['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
|
||||||
|
['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
|
||||||
|
['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
|
||||||
|
['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($extensions as &$extension) {
|
||||||
|
$extension['loaded'] = extension_loaded($extension['name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the EOL date of given PHP version. If the version is unknown,
|
||||||
|
* we return today + 2 years.
|
||||||
|
*
|
||||||
|
* @param string $fullVersion PHP version, e.g. 7.4.7
|
||||||
|
*
|
||||||
|
* @return string Date format: YYYY-MM-DD
|
||||||
|
*/
|
||||||
|
public static function getPhpEol(string $fullVersion): string
|
||||||
|
{
|
||||||
|
preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'7.1' => '2019-12-01',
|
||||||
|
'7.2' => '2020-11-30',
|
||||||
|
'7.3' => '2021-12-06',
|
||||||
|
'7.4' => '2022-11-28',
|
||||||
|
'8.0' => '2023-12-01',
|
||||||
|
][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
|
||||||
|
}
|
||||||
}
|
}
|
236
application/helper/DailyPageHelper.php
Normal file
236
application/helper/DailyPageHelper.php
Normal file
|
@ -0,0 +1,236 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Helper;
|
||||||
|
|
||||||
|
use DatePeriod;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Exception;
|
||||||
|
use Shaarli\Bookmark\Bookmark;
|
||||||
|
use Slim\Http\Request;
|
||||||
|
|
||||||
|
class DailyPageHelper
|
||||||
|
{
|
||||||
|
public const MONTH = 'month';
|
||||||
|
public const WEEK = 'week';
|
||||||
|
public const DAY = 'day';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the type of the daily to display from the HTTP request parameters
|
||||||
|
*
|
||||||
|
* @param Request $request HTTP request
|
||||||
|
*
|
||||||
|
* @return string month/week/day
|
||||||
|
*/
|
||||||
|
public static function extractRequestedType(Request $request): string
|
||||||
|
{
|
||||||
|
if ($request->getQueryParam(static::MONTH) !== null) {
|
||||||
|
return static::MONTH;
|
||||||
|
} elseif ($request->getQueryParam(static::WEEK) !== null) {
|
||||||
|
return static::WEEK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static::DAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a DateTimeImmutable from provided HTTP request.
|
||||||
|
* If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
|
||||||
|
* If the datastore is empty or no bookmark is provided, we use the current date.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
* @param string|null $requestedDate Input string extracted from the request
|
||||||
|
* @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
|
||||||
|
*
|
||||||
|
* @return DateTimeImmutable from input or latest bookmark.
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function extractRequestedDateTime(
|
||||||
|
string $type,
|
||||||
|
?string $requestedDate,
|
||||||
|
Bookmark $latestBookmark = null
|
||||||
|
): DateTimeImmutable {
|
||||||
|
$format = static::getFormatByType($type);
|
||||||
|
if (empty($requestedDate)) {
|
||||||
|
return $latestBookmark instanceof Bookmark
|
||||||
|
? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
|
||||||
|
: new DateTimeImmutable()
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
// W is not supported by createFromFormat...
|
||||||
|
if ($type === static::WEEK) {
|
||||||
|
return (new DateTimeImmutable())
|
||||||
|
->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeImmutable::createFromFormat($format, $requestedDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the DateTime format used by provided type
|
||||||
|
* Examples:
|
||||||
|
* - day: 20201016 (<year><month><day>)
|
||||||
|
* - week: 202041 (<year><week number>)
|
||||||
|
* - month: 202010 (<year><month>)
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
*
|
||||||
|
* @return string DateTime compatible format
|
||||||
|
*
|
||||||
|
* @see https://www.php.net/manual/en/datetime.format.php
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getFormatByType(string $type): string
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case static::MONTH:
|
||||||
|
return 'Ym';
|
||||||
|
case static::WEEK:
|
||||||
|
return 'YW';
|
||||||
|
case static::DAY:
|
||||||
|
return 'Ymd';
|
||||||
|
default:
|
||||||
|
throw new Exception('Unsupported daily format type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first DateTime of the time period depending on given datetime and type.
|
||||||
|
* Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
|
||||||
|
* and we don't want to alter original datetime.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
* @param DateTimeImmutable $requested DateTime extracted from request input
|
||||||
|
* (should come from extractRequestedDateTime)
|
||||||
|
*
|
||||||
|
* @return \DateTimeInterface First DateTime of the time period
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case static::MONTH:
|
||||||
|
return $requested->modify('first day of this month midnight');
|
||||||
|
case static::WEEK:
|
||||||
|
return $requested->modify('Monday this week midnight');
|
||||||
|
case static::DAY:
|
||||||
|
return $requested->modify('Today midnight');
|
||||||
|
default:
|
||||||
|
throw new Exception('Unsupported daily format type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last DateTime of the time period depending on given datetime and type.
|
||||||
|
* Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
|
||||||
|
* and we don't want to alter original datetime.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
* @param DateTimeImmutable $requested DateTime extracted from request input
|
||||||
|
* (should come from extractRequestedDateTime)
|
||||||
|
*
|
||||||
|
* @return \DateTimeInterface Last DateTime of the time period
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case static::MONTH:
|
||||||
|
return $requested->modify('last day of this month 23:59:59');
|
||||||
|
case static::WEEK:
|
||||||
|
return $requested->modify('Sunday this week 23:59:59');
|
||||||
|
case static::DAY:
|
||||||
|
return $requested->modify('Today 23:59:59');
|
||||||
|
default:
|
||||||
|
throw new Exception('Unsupported daily format type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get localized description of the time period depending on given datetime and type.
|
||||||
|
* Example: for a month period, it returns `October, 2020`.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
* @param \DateTimeImmutable $requested DateTime extracted from request input
|
||||||
|
* (should come from extractRequestedDateTime)
|
||||||
|
* @param bool $includeRelative Include relative date description (today, yesterday, etc.)
|
||||||
|
*
|
||||||
|
* @return string Localized time period description
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getDescriptionByType(
|
||||||
|
string $type,
|
||||||
|
\DateTimeImmutable $requested,
|
||||||
|
bool $includeRelative = true
|
||||||
|
): string {
|
||||||
|
switch ($type) {
|
||||||
|
case static::MONTH:
|
||||||
|
return $requested->format('F') . ', ' . $requested->format('Y');
|
||||||
|
case static::WEEK:
|
||||||
|
$requested = $requested->modify('Monday this week');
|
||||||
|
return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
|
||||||
|
case static::DAY:
|
||||||
|
$out = '';
|
||||||
|
if ($includeRelative && $requested->format('Ymd') === date('Ymd')) {
|
||||||
|
$out = t('Today') . ' - ';
|
||||||
|
} elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
|
||||||
|
$out = t('Yesterday') . ' - ';
|
||||||
|
}
|
||||||
|
return $out . format_date($requested, false);
|
||||||
|
default:
|
||||||
|
throw new Exception('Unsupported daily format type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of items to display in the RSS feed depending on the given type.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
*
|
||||||
|
* @return int number of elements
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getRssLengthByType(string $type): int
|
||||||
|
{
|
||||||
|
switch ($type) {
|
||||||
|
case static::MONTH:
|
||||||
|
return 12; // 1 year
|
||||||
|
case static::WEEK:
|
||||||
|
return 26; // ~6 months
|
||||||
|
case static::DAY:
|
||||||
|
return 30; // ~1 month
|
||||||
|
default:
|
||||||
|
throw new Exception('Unsupported daily format type');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of items to display in the RSS feed depending on the given type.
|
||||||
|
*
|
||||||
|
* @param string $type month/week/day
|
||||||
|
* @param ?DateTimeImmutable $requested Currently only used for UT
|
||||||
|
*
|
||||||
|
* @return DatePeriod number of elements
|
||||||
|
*
|
||||||
|
* @throws Exception Type not supported.
|
||||||
|
*/
|
||||||
|
public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
|
||||||
|
{
|
||||||
|
$requested = $requested ?? new DateTimeImmutable();
|
||||||
|
|
||||||
|
return new DatePeriod(
|
||||||
|
static::getStartDateTimeByType($type, $requested),
|
||||||
|
new \DateInterval('P1D'),
|
||||||
|
static::getEndDateTimeByType($type, $requested)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli;
|
namespace Shaarli\Helper;
|
||||||
|
|
||||||
use Shaarli\Exceptions\IOException;
|
use Shaarli\Exceptions\IOException;
|
||||||
|
|
||||||
|
@ -81,4 +81,60 @@ public static function readFlatDB($file, $default = null)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively deletes a folder content, and deletes itself optionally.
|
||||||
|
* If an excluded file is found, folders won't be deleted.
|
||||||
|
*
|
||||||
|
* Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @param bool $selfDelete Delete the provided folder if true, only its content if false.
|
||||||
|
* @param array $exclude
|
||||||
|
*/
|
||||||
|
public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
|
||||||
|
{
|
||||||
|
$skipped = false;
|
||||||
|
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
throw new IOException(t('Provided path is not a directory.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!static::isPathInShaarliFolder($path)) {
|
||||||
|
throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (new \DirectoryIterator($path) as $file) {
|
||||||
|
if ($file->isDot()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($file->getBasename(), $exclude, true)) {
|
||||||
|
$skipped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($file->isFile()) {
|
||||||
|
unlink($file->getPathname());
|
||||||
|
} elseif ($file->isDir()) {
|
||||||
|
$skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($selfDelete && !$skipped) {
|
||||||
|
rmdir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $skipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the given path is inside Shaarli directory.
|
||||||
|
*/
|
||||||
|
public static function isPathInShaarliFolder(string $path): bool
|
||||||
|
{
|
||||||
|
$rootDirectory = dirname(dirname(dirname(__FILE__)));
|
||||||
|
|
||||||
|
return strpos(realpath($path), $rootDirectory) !== false;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,9 +14,14 @@
|
||||||
*/
|
*/
|
||||||
class HttpAccess
|
class HttpAccess
|
||||||
{
|
{
|
||||||
public function getHttpResponse($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
|
public function getHttpResponse(
|
||||||
{
|
$url,
|
||||||
return get_http_response($url, $timeout, $maxBytes, $curlWriteFunction);
|
$timeout = 30,
|
||||||
|
$maxBytes = 4194304,
|
||||||
|
$curlHeaderFunction = null,
|
||||||
|
$curlWriteFunction = null
|
||||||
|
) {
|
||||||
|
return get_http_response($url, $timeout, $maxBytes, $curlHeaderFunction, $curlWriteFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCurlDownloadCallback(
|
public function getCurlDownloadCallback(
|
||||||
|
@ -25,7 +30,7 @@ public function getCurlDownloadCallback(
|
||||||
&$description,
|
&$description,
|
||||||
&$keywords,
|
&$keywords,
|
||||||
$retrieveDescription,
|
$retrieveDescription,
|
||||||
$curlGetInfo = 'curl_getinfo'
|
$tagsSeparator
|
||||||
) {
|
) {
|
||||||
return get_curl_download_callback(
|
return get_curl_download_callback(
|
||||||
$charset,
|
$charset,
|
||||||
|
@ -33,7 +38,12 @@ public function getCurlDownloadCallback(
|
||||||
$description,
|
$description,
|
||||||
$keywords,
|
$keywords,
|
||||||
$retrieveDescription,
|
$retrieveDescription,
|
||||||
$curlGetInfo
|
$tagsSeparator
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCurlHeaderCallback(&$charset, $curlGetInfo = 'curl_getinfo')
|
||||||
|
{
|
||||||
|
return get_curl_header_callback($charset, $curlGetInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,14 @@
|
||||||
* GET an HTTP URL to retrieve its content
|
* GET an HTTP URL to retrieve its content
|
||||||
* Uses the cURL library or a fallback method
|
* Uses the cURL library or a fallback method
|
||||||
*
|
*
|
||||||
* @param string $url URL to get (http://...)
|
* @param string $url URL to get (http://...)
|
||||||
* @param int $timeout network timeout (in seconds)
|
* @param int $timeout network timeout (in seconds)
|
||||||
* @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
|
* @param int $maxBytes maximum downloaded bytes (default: 4 MiB)
|
||||||
* @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
|
* @param callable|string $curlHeaderFunction Optional callback called during the download of headers
|
||||||
* Can be used to add download conditions on the
|
* (CURLOPT_HEADERFUNCTION)
|
||||||
* headers (response code, content type, etc.).
|
* @param callable|string $curlWriteFunction Optional callback called during the download (cURL CURLOPT_WRITEFUNCTION).
|
||||||
|
* Can be used to add download conditions on the
|
||||||
|
* headers (response code, content type, etc.).
|
||||||
*
|
*
|
||||||
* @return array HTTP response headers, downloaded content
|
* @return array HTTP response headers, downloaded content
|
||||||
*
|
*
|
||||||
|
@ -35,13 +37,18 @@
|
||||||
* @see http://stackoverflow.com/q/9183178
|
* @see http://stackoverflow.com/q/9183178
|
||||||
* @see http://stackoverflow.com/q/1462720
|
* @see http://stackoverflow.com/q/1462720
|
||||||
*/
|
*/
|
||||||
function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteFunction = null)
|
function get_http_response(
|
||||||
{
|
$url,
|
||||||
|
$timeout = 30,
|
||||||
|
$maxBytes = 4194304,
|
||||||
|
$curlHeaderFunction = null,
|
||||||
|
$curlWriteFunction = null
|
||||||
|
) {
|
||||||
$urlObj = new Url($url);
|
$urlObj = new Url($url);
|
||||||
$cleanUrl = $urlObj->idnToAscii();
|
$cleanUrl = $urlObj->idnToAscii();
|
||||||
|
|
||||||
if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
|
if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
|
||||||
return array(array(0 => 'Invalid HTTP UrlUtils'), false);
|
return [[0 => 'Invalid HTTP UrlUtils'], false];
|
||||||
}
|
}
|
||||||
|
|
||||||
$userAgent =
|
$userAgent =
|
||||||
|
@ -64,42 +71,39 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
|
||||||
|
|
||||||
$ch = curl_init($cleanUrl);
|
$ch = curl_init($cleanUrl);
|
||||||
if ($ch === false) {
|
if ($ch === false) {
|
||||||
return array(array(0 => 'curl_init() error'), false);
|
return [[0 => 'curl_init() error'], false];
|
||||||
}
|
}
|
||||||
|
|
||||||
// General cURL settings
|
// General cURL settings
|
||||||
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
|
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
|
||||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
// Default header download if the $curlHeaderFunction is not defined
|
||||||
|
curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction));
|
||||||
curl_setopt(
|
curl_setopt(
|
||||||
$ch,
|
$ch,
|
||||||
CURLOPT_HTTPHEADER,
|
CURLOPT_HTTPHEADER,
|
||||||
array('Accept-Language: ' . $acceptLanguage)
|
['Accept-Language: ' . $acceptLanguage]
|
||||||
);
|
);
|
||||||
curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
|
curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||||
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
|
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
|
||||||
|
|
||||||
|
// Max download size management
|
||||||
|
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16);
|
||||||
|
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
|
||||||
|
if (is_callable($curlHeaderFunction)) {
|
||||||
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction);
|
||||||
|
}
|
||||||
if (is_callable($curlWriteFunction)) {
|
if (is_callable($curlWriteFunction)) {
|
||||||
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
|
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max download size management
|
|
||||||
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
|
|
||||||
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
|
|
||||||
curl_setopt(
|
curl_setopt(
|
||||||
$ch,
|
$ch,
|
||||||
CURLOPT_PROGRESSFUNCTION,
|
CURLOPT_PROGRESSFUNCTION,
|
||||||
function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) {
|
||||||
if (version_compare(phpversion(), '5.5', '<')) {
|
$downloaded = $arg2;
|
||||||
// PHP version lower than 5.5
|
|
||||||
// Callback has 4 arguments
|
|
||||||
$downloaded = $arg1;
|
|
||||||
} else {
|
|
||||||
// Callback has 5 arguments
|
|
||||||
$downloaded = $arg2;
|
|
||||||
}
|
|
||||||
// Non-zero return stops downloading
|
// Non-zero return stops downloading
|
||||||
return ($downloaded > $maxBytes) ? 1 : 0;
|
return ($downloaded > $maxBytes) ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
@ -118,9 +122,9 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
||||||
* Removing this would require updating
|
* Removing this would require updating
|
||||||
* GetHttpUrlTest::testGetInvalidRemoteUrl()
|
* GetHttpUrlTest::testGetInvalidRemoteUrl()
|
||||||
*/
|
*/
|
||||||
return array(false, false);
|
return [false, false];
|
||||||
}
|
}
|
||||||
return array(array(0 => 'curl_exec() error: ' . $errorStr), false);
|
return [[0 => 'curl_exec() error: ' . $errorStr], false];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Formatting output like the fallback method
|
// Formatting output like the fallback method
|
||||||
|
@ -131,7 +135,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
||||||
$rawHeadersLastRedir = end($rawHeadersArrayRedirs);
|
$rawHeadersLastRedir = end($rawHeadersArrayRedirs);
|
||||||
|
|
||||||
$content = substr($response, $headSize);
|
$content = substr($response, $headSize);
|
||||||
$headers = array();
|
$headers = [];
|
||||||
foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
|
foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
|
||||||
if (empty($line) || ctype_space($line)) {
|
if (empty($line) || ctype_space($line)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -142,7 +146,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
||||||
$value = $splitLine[1];
|
$value = $splitLine[1];
|
||||||
if (array_key_exists($key, $headers)) {
|
if (array_key_exists($key, $headers)) {
|
||||||
if (!is_array($headers[$key])) {
|
if (!is_array($headers[$key])) {
|
||||||
$headers[$key] = array(0 => $headers[$key]);
|
$headers[$key] = [0 => $headers[$key]];
|
||||||
}
|
}
|
||||||
$headers[$key][] = $value;
|
$headers[$key][] = $value;
|
||||||
} else {
|
} else {
|
||||||
|
@ -153,7 +157,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return array($headers, $content);
|
return [$headers, $content];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,15 +188,15 @@ function get_http_response_fallback(
|
||||||
$acceptLanguage,
|
$acceptLanguage,
|
||||||
$maxRedr
|
$maxRedr
|
||||||
) {
|
) {
|
||||||
$options = array(
|
$options = [
|
||||||
'http' => array(
|
'http' => [
|
||||||
'method' => 'GET',
|
'method' => 'GET',
|
||||||
'timeout' => $timeout,
|
'timeout' => $timeout,
|
||||||
'user_agent' => $userAgent,
|
'user_agent' => $userAgent,
|
||||||
'header' => "Accept: */*\r\n"
|
'header' => "Accept: */*\r\n"
|
||||||
. 'Accept-Language: ' . $acceptLanguage
|
. 'Accept-Language: ' . $acceptLanguage
|
||||||
)
|
]
|
||||||
);
|
];
|
||||||
|
|
||||||
stream_context_set_default($options);
|
stream_context_set_default($options);
|
||||||
list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
|
list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
|
||||||
|
@ -203,7 +207,7 @@ function get_http_response_fallback(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $headers) {
|
if (! $headers) {
|
||||||
return array($headers, false);
|
return [$headers, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -211,10 +215,10 @@ function get_http_response_fallback(
|
||||||
$context = stream_context_create($options);
|
$context = stream_context_create($options);
|
||||||
$content = file_get_contents($finalUrl, false, $context, -1, $maxBytes);
|
$content = file_get_contents($finalUrl, false, $context, -1, $maxBytes);
|
||||||
} catch (Exception $exc) {
|
} catch (Exception $exc) {
|
||||||
return array(array(0 => 'HTTP Error'), $exc->getMessage());
|
return [[0 => 'HTTP Error'], $exc->getMessage()];
|
||||||
}
|
}
|
||||||
|
|
||||||
return array($headers, $content);
|
return [$headers, $content];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -233,10 +237,12 @@ function get_redirected_headers($url, $redirectionLimit = 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headers found, redirection found, and limit not reached.
|
// Headers found, redirection found, and limit not reached.
|
||||||
if ($redirectionLimit-- > 0
|
if (
|
||||||
|
$redirectionLimit-- > 0
|
||||||
&& !empty($headers)
|
&& !empty($headers)
|
||||||
&& (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
|
&& (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
|
||||||
&& !empty($headers['Location'])) {
|
&& !empty($headers['Location'])
|
||||||
|
) {
|
||||||
$redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
|
$redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
|
||||||
if ($redirection != $url) {
|
if ($redirection != $url) {
|
||||||
$redirection = getAbsoluteUrl($url, $redirection);
|
$redirection = getAbsoluteUrl($url, $redirection);
|
||||||
|
@ -244,7 +250,7 @@ function get_redirected_headers($url, $redirectionLimit = 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return array($headers, $url);
|
return [$headers, $url];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -266,7 +272,7 @@ function getAbsoluteUrl($originalUrl, $newUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
$parts = parse_url($originalUrl);
|
$parts = parse_url($originalUrl);
|
||||||
$final = $parts['scheme'] .'://'. $parts['host'];
|
$final = $parts['scheme'] . '://' . $parts['host'];
|
||||||
$final .= (!empty($parts['port'])) ? $parts['port'] : '';
|
$final .= (!empty($parts['port'])) ? $parts['port'] : '';
|
||||||
$final .= '/';
|
$final .= '/';
|
||||||
if ($newUrl[0] != '/') {
|
if ($newUrl[0] != '/') {
|
||||||
|
@ -319,7 +325,8 @@ function server_url($server)
|
||||||
$scheme = 'https';
|
$scheme = 'https';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($scheme == 'http' && $port != '80')
|
if (
|
||||||
|
($scheme == 'http' && $port != '80')
|
||||||
|| ($scheme == 'https' && $port != '443')
|
|| ($scheme == 'https' && $port != '443')
|
||||||
) {
|
) {
|
||||||
$port = ':' . $port;
|
$port = ':' . $port;
|
||||||
|
@ -340,22 +347,26 @@ function server_url($server)
|
||||||
$host = $server['SERVER_NAME'];
|
$host = $server['SERVER_NAME'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $scheme.'://'.$host.$port;
|
return $scheme . '://' . $host . $port;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL detection
|
// SSL detection
|
||||||
if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on')
|
if (
|
||||||
|| (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) {
|
(! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on')
|
||||||
|
|| (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')
|
||||||
|
) {
|
||||||
$scheme = 'https';
|
$scheme = 'https';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not append standard port values
|
// Do not append standard port values
|
||||||
if (($scheme == 'http' && $server['SERVER_PORT'] != '80')
|
if (
|
||||||
|| ($scheme == 'https' && $server['SERVER_PORT'] != '443')) {
|
($scheme == 'http' && $server['SERVER_PORT'] != '80')
|
||||||
$port = ':'.$server['SERVER_PORT'];
|
|| ($scheme == 'https' && $server['SERVER_PORT'] != '443')
|
||||||
|
) {
|
||||||
|
$port = ':' . $server['SERVER_PORT'];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $scheme.'://'.$server['SERVER_NAME'].$port;
|
return $scheme . '://' . $server['SERVER_NAME'] . $port;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -493,53 +504,22 @@ function is_https($server)
|
||||||
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
||||||
*
|
*
|
||||||
* @param string $charset to extract from the downloaded page (reference)
|
* @param string $charset to extract from the downloaded page (reference)
|
||||||
* @param string $title to extract from the downloaded page (reference)
|
|
||||||
* @param string $description to extract from the downloaded page (reference)
|
|
||||||
* @param string $keywords to extract from the downloaded page (reference)
|
|
||||||
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
|
|
||||||
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
||||||
*
|
*
|
||||||
* @return Closure
|
* @return Closure
|
||||||
*/
|
*/
|
||||||
function get_curl_download_callback(
|
function get_curl_header_callback(
|
||||||
&$charset,
|
&$charset,
|
||||||
&$title,
|
|
||||||
&$description,
|
|
||||||
&$keywords,
|
|
||||||
$retrieveDescription,
|
|
||||||
$curlGetInfo = 'curl_getinfo'
|
$curlGetInfo = 'curl_getinfo'
|
||||||
) {
|
) {
|
||||||
$isRedirected = false;
|
$isRedirected = false;
|
||||||
$currentChunk = 0;
|
|
||||||
$foundChunk = null;
|
|
||||||
|
|
||||||
/**
|
return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) {
|
||||||
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
|
|
||||||
*
|
|
||||||
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
|
|
||||||
* Then we extract the title and the charset and stop the download when it's done.
|
|
||||||
*
|
|
||||||
* @param resource $ch cURL resource
|
|
||||||
* @param string $data chunk of data being downloaded
|
|
||||||
*
|
|
||||||
* @return int|bool length of $data or false if we need to stop the download
|
|
||||||
*/
|
|
||||||
return function (&$ch, $data) use (
|
|
||||||
$retrieveDescription,
|
|
||||||
$curlGetInfo,
|
|
||||||
&$charset,
|
|
||||||
&$title,
|
|
||||||
&$description,
|
|
||||||
&$keywords,
|
|
||||||
&$isRedirected,
|
|
||||||
&$currentChunk,
|
|
||||||
&$foundChunk
|
|
||||||
) {
|
|
||||||
$currentChunk++;
|
|
||||||
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
|
$responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
|
||||||
|
$chunkLength = strlen($data);
|
||||||
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
|
if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
|
||||||
$isRedirected = true;
|
$isRedirected = true;
|
||||||
return strlen($data);
|
return $chunkLength;
|
||||||
}
|
}
|
||||||
if (!empty($responseCode) && $responseCode !== 200) {
|
if (!empty($responseCode) && $responseCode !== 200) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -555,6 +535,61 @@ function get_curl_download_callback(
|
||||||
if (!empty($contentType) && empty($charset)) {
|
if (!empty($contentType) && empty($charset)) {
|
||||||
$charset = header_extract_charset($contentType);
|
$charset = header_extract_charset($contentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $chunkLength;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cURL callback function for CURLOPT_WRITEFUNCTION
|
||||||
|
*
|
||||||
|
* @param string $charset to extract from the downloaded page (reference)
|
||||||
|
* @param string $title to extract from the downloaded page (reference)
|
||||||
|
* @param string $description to extract from the downloaded page (reference)
|
||||||
|
* @param string $keywords to extract from the downloaded page (reference)
|
||||||
|
* @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
|
||||||
|
* @param string $curlGetInfo Optionally overrides curl_getinfo function
|
||||||
|
*
|
||||||
|
* @return Closure
|
||||||
|
*/
|
||||||
|
function get_curl_download_callback(
|
||||||
|
&$charset,
|
||||||
|
&$title,
|
||||||
|
&$description,
|
||||||
|
&$keywords,
|
||||||
|
$retrieveDescription,
|
||||||
|
$tagsSeparator
|
||||||
|
) {
|
||||||
|
$currentChunk = 0;
|
||||||
|
$foundChunk = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
|
||||||
|
*
|
||||||
|
* While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
|
||||||
|
* Then we extract the title and the charset and stop the download when it's done.
|
||||||
|
*
|
||||||
|
* @param resource $ch cURL resource
|
||||||
|
* @param string $data chunk of data being downloaded
|
||||||
|
*
|
||||||
|
* @return int|bool length of $data or false if we need to stop the download
|
||||||
|
*/
|
||||||
|
return function (
|
||||||
|
$ch,
|
||||||
|
$data
|
||||||
|
) use (
|
||||||
|
$retrieveDescription,
|
||||||
|
$tagsSeparator,
|
||||||
|
&$charset,
|
||||||
|
&$title,
|
||||||
|
&$description,
|
||||||
|
&$keywords,
|
||||||
|
&$currentChunk,
|
||||||
|
&$foundChunk
|
||||||
|
) {
|
||||||
|
$chunkLength = strlen($data);
|
||||||
|
$currentChunk++;
|
||||||
|
|
||||||
if (empty($charset)) {
|
if (empty($charset)) {
|
||||||
$charset = html_extract_charset($data);
|
$charset = html_extract_charset($data);
|
||||||
}
|
}
|
||||||
|
@ -562,6 +597,10 @@ function get_curl_download_callback(
|
||||||
$title = html_extract_title($data);
|
$title = html_extract_title($data);
|
||||||
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
||||||
}
|
}
|
||||||
|
if (empty($title)) {
|
||||||
|
$title = html_extract_tag('title', $data);
|
||||||
|
$foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
|
||||||
|
}
|
||||||
if ($retrieveDescription && empty($description)) {
|
if ($retrieveDescription && empty($description)) {
|
||||||
$description = html_extract_tag('description', $data);
|
$description = html_extract_tag('description', $data);
|
||||||
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
|
$foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
|
||||||
|
@ -571,10 +610,10 @@ function get_curl_download_callback(
|
||||||
if (! empty($keywords)) {
|
if (! empty($keywords)) {
|
||||||
$foundChunk = $currentChunk;
|
$foundChunk = $currentChunk;
|
||||||
// Keywords use the format tag1, tag2 multiple words, tag
|
// Keywords use the format tag1, tag2 multiple words, tag
|
||||||
// So we format them to match Shaarli's separator and glue multiple words with '-'
|
// So we split the result with `,`, then if a tag contains the separator we replace it by `-`.
|
||||||
$keywords = implode(' ', array_map(function($keyword) {
|
$keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string {
|
||||||
return implode('-', preg_split('/\s+/', trim($keyword)));
|
return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-');
|
||||||
}, explode(',', $keywords)));
|
}, tags_str2array($keywords, ',')), $tagsSeparator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,7 +621,8 @@ function get_curl_download_callback(
|
||||||
// If we already found either the title, description or keywords,
|
// If we already found either the title, description or keywords,
|
||||||
// it's highly unlikely that we'll found the other metas further than
|
// it's highly unlikely that we'll found the other metas further than
|
||||||
// in the same chunk of data or the next one. So we also stop the download after that.
|
// in the same chunk of data or the next one. So we also stop the download after that.
|
||||||
if ((!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
|
if (
|
||||||
|
(!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
|
||||||
&& (! $retrieveDescription
|
&& (! $retrieveDescription
|
||||||
|| $foundChunk < $currentChunk
|
|| $foundChunk < $currentChunk
|
||||||
|| (!empty($title) && !empty($description) && !empty($keywords))
|
|| (!empty($title) && !empty($description) && !empty($keywords))
|
||||||
|
@ -591,6 +631,6 @@ function get_curl_download_callback(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return strlen($data);
|
return $chunkLength;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
74
application/http/MetadataRetriever.php
Normal file
74
application/http/MetadataRetriever.php
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Http;
|
||||||
|
|
||||||
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Tool used to extract metadata from external URL (title, description, etc.).
|
||||||
|
*/
|
||||||
|
class MetadataRetriever
|
||||||
|
{
|
||||||
|
/** @var ConfigManager */
|
||||||
|
protected $conf;
|
||||||
|
|
||||||
|
/** @var HttpAccess */
|
||||||
|
protected $httpAccess;
|
||||||
|
|
||||||
|
public function __construct(ConfigManager $conf, HttpAccess $httpAccess)
|
||||||
|
{
|
||||||
|
$this->conf = $conf;
|
||||||
|
$this->httpAccess = $httpAccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve metadata for given URL.
|
||||||
|
*
|
||||||
|
* @return array [
|
||||||
|
* 'title' => <remote title>,
|
||||||
|
* 'description' => <remote description>,
|
||||||
|
* 'tags' => <remote keywords>,
|
||||||
|
* ]
|
||||||
|
*/
|
||||||
|
public function retrieve(string $url): array
|
||||||
|
{
|
||||||
|
$charset = null;
|
||||||
|
$title = null;
|
||||||
|
$description = null;
|
||||||
|
$tags = null;
|
||||||
|
|
||||||
|
// Short timeout to keep the application responsive
|
||||||
|
// The callback will fill $charset and $title with data from the downloaded page.
|
||||||
|
$this->httpAccess->getHttpResponse(
|
||||||
|
$url,
|
||||||
|
$this->conf->get('general.download_timeout', 30),
|
||||||
|
$this->conf->get('general.download_max_size', 4194304),
|
||||||
|
$this->httpAccess->getCurlHeaderCallback($charset),
|
||||||
|
$this->httpAccess->getCurlDownloadCallback(
|
||||||
|
$charset,
|
||||||
|
$title,
|
||||||
|
$description,
|
||||||
|
$tags,
|
||||||
|
$this->conf->get('general.retrieve_description'),
|
||||||
|
$this->conf->get('general.tags_separator', ' ')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($title) && strtolower($charset) !== 'utf-8') {
|
||||||
|
$title = mb_convert_encoding($title, 'utf-8', $charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_map([$this, 'cleanMetadata'], [
|
||||||
|
'title' => $title,
|
||||||
|
'description' => $description,
|
||||||
|
'tags' => $tags,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function cleanMetadata($data): ?string
|
||||||
|
{
|
||||||
|
return !is_string($data) || empty(trim($data)) ? null : trim($data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
*/
|
*/
|
||||||
class Url
|
class Url
|
||||||
{
|
{
|
||||||
private static $annoyingQueryParams = array(
|
private static $annoyingQueryParams = [
|
||||||
// Facebook
|
// Facebook
|
||||||
'action_object_map=',
|
'action_object_map=',
|
||||||
'action_ref_map=',
|
'action_ref_map=',
|
||||||
|
@ -37,15 +37,15 @@ class Url
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
'campaign_'
|
'campaign_'
|
||||||
);
|
];
|
||||||
|
|
||||||
private static $annoyingFragments = array(
|
private static $annoyingFragments = [
|
||||||
// ATInternet
|
// ATInternet
|
||||||
'xtor=RSS-',
|
'xtor=RSS-',
|
||||||
|
|
||||||
// Misc.
|
// Misc.
|
||||||
'tk.rss_all'
|
'tk.rss_all'
|
||||||
);
|
];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* URL parts represented as an array
|
* URL parts represented as an array
|
||||||
|
@ -120,7 +120,7 @@ protected function cleanupQuery()
|
||||||
foreach (self::$annoyingQueryParams as $annoying) {
|
foreach (self::$annoyingQueryParams as $annoying) {
|
||||||
foreach ($queryParams as $param) {
|
foreach ($queryParams as $param) {
|
||||||
if (startsWith($param, $annoying)) {
|
if (startsWith($param, $annoying)) {
|
||||||
$queryParams = array_diff($queryParams, array($param));
|
$queryParams = array_diff($queryParams, [$param]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an array-represented URL to a string
|
* Converts an array-represented URL to a string
|
||||||
*
|
*
|
||||||
|
@ -12,15 +13,15 @@
|
||||||
*/
|
*/
|
||||||
function unparse_url($parsedUrl)
|
function unparse_url($parsedUrl)
|
||||||
{
|
{
|
||||||
$scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'].'://' : '';
|
$scheme = isset($parsedUrl['scheme']) ? $parsedUrl['scheme'] . '://' : '';
|
||||||
$host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
|
$host = isset($parsedUrl['host']) ? $parsedUrl['host'] : '';
|
||||||
$port = isset($parsedUrl['port']) ? ':'.$parsedUrl['port'] : '';
|
$port = isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
|
||||||
$user = isset($parsedUrl['user']) ? $parsedUrl['user'] : '';
|
$user = isset($parsedUrl['user']) ? $parsedUrl['user'] : '';
|
||||||
$pass = isset($parsedUrl['pass']) ? ':'.$parsedUrl['pass'] : '';
|
$pass = isset($parsedUrl['pass']) ? ':' . $parsedUrl['pass'] : '';
|
||||||
$pass = ($user || $pass) ? "$pass@" : '';
|
$pass = ($user || $pass) ? "$pass@" : '';
|
||||||
$path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
|
$path = isset($parsedUrl['path']) ? $parsedUrl['path'] : '';
|
||||||
$query = isset($parsedUrl['query']) ? '?'.$parsedUrl['query'] : '';
|
$query = isset($parsedUrl['query']) ? '?' . $parsedUrl['query'] : '';
|
||||||
$fragment = isset($parsedUrl['fragment']) ? '#'.$parsedUrl['fragment'] : '';
|
$fragment = isset($parsedUrl['fragment']) ? '#' . $parsedUrl['fragment'] : '';
|
||||||
|
|
||||||
return "$scheme$user$pass$host$port$path$query$fragment";
|
return "$scheme$user$pass$host$port$path$query$fragment";
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ public function post(Request $request, Response $response): Response
|
||||||
|
|
||||||
if (!$this->container->loginManager->isLoggedIn()) {
|
if (!$this->container->loginManager->isLoggedIn()) {
|
||||||
$parameters = $buildParameters($request->getQueryParams(), true);
|
$parameters = $buildParameters($request->getQueryParams(), true);
|
||||||
return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters);
|
return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route . $parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
$parameters = $buildParameters($request->getQueryParams(), false);
|
$parameters = $buildParameters($request->getQueryParams(), false);
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
use Iterator;
|
use Iterator;
|
||||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||||
use Shaarli\Exceptions\IOException;
|
use Shaarli\Exceptions\IOException;
|
||||||
use Shaarli\FileUtils;
|
use Shaarli\Helper\FileUtils;
|
||||||
use Shaarli\Render\PageCacheManager;
|
use Shaarli\Render\PageCacheManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +62,7 @@ class LegacyLinkDB implements Iterator, Countable, ArrayAccess
|
||||||
private $datastore;
|
private $datastore;
|
||||||
|
|
||||||
// Link date storage format
|
// Link date storage format
|
||||||
const LINK_DATE_FORMAT = 'Ymd_His';
|
public const LINK_DATE_FORMAT = 'Ymd_His';
|
||||||
|
|
||||||
// List of bookmarks (associative array)
|
// List of bookmarks (associative array)
|
||||||
// - key: link date (e.g. "20110823_124546"),
|
// - key: link date (e.g. "20110823_124546"),
|
||||||
|
@ -240,8 +240,8 @@ private function check()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a dummy database for example
|
// Create a dummy database for example
|
||||||
$this->links = array();
|
$this->links = [];
|
||||||
$link = array(
|
$link = [
|
||||||
'id' => 1,
|
'id' => 1,
|
||||||
'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
|
'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
|
||||||
'url' => 'https://shaarli.readthedocs.io',
|
'url' => 'https://shaarli.readthedocs.io',
|
||||||
|
@ -257,11 +257,11 @@ private function check()
|
||||||
'created' => new DateTime(),
|
'created' => new DateTime(),
|
||||||
'tags' => 'opensource software',
|
'tags' => 'opensource software',
|
||||||
'sticky' => false,
|
'sticky' => false,
|
||||||
);
|
];
|
||||||
$link['shorturl'] = link_small_hash($link['created'], $link['id']);
|
$link['shorturl'] = link_small_hash($link['created'], $link['id']);
|
||||||
$this->links[1] = $link;
|
$this->links[1] = $link;
|
||||||
|
|
||||||
$link = array(
|
$link = [
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'title' => t('My secret stuff... - Pastebin.com'),
|
'title' => t('My secret stuff... - Pastebin.com'),
|
||||||
'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
|
'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
|
||||||
|
@ -270,7 +270,7 @@ private function check()
|
||||||
'created' => new DateTime('1 minute ago'),
|
'created' => new DateTime('1 minute ago'),
|
||||||
'tags' => 'secretstuff',
|
'tags' => 'secretstuff',
|
||||||
'sticky' => false,
|
'sticky' => false,
|
||||||
);
|
];
|
||||||
$link['shorturl'] = link_small_hash($link['created'], $link['id']);
|
$link['shorturl'] = link_small_hash($link['created'], $link['id']);
|
||||||
$this->links[0] = $link;
|
$this->links[0] = $link;
|
||||||
|
|
||||||
|
@ -285,7 +285,7 @@ private function read()
|
||||||
{
|
{
|
||||||
// Public bookmarks are hidden and user not logged in => nothing to show
|
// Public bookmarks are hidden and user not logged in => nothing to show
|
||||||
if ($this->hidePublicLinks && !$this->loggedIn) {
|
if ($this->hidePublicLinks && !$this->loggedIn) {
|
||||||
$this->links = array();
|
$this->links = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +293,7 @@ private function read()
|
||||||
$this->ids = [];
|
$this->ids = [];
|
||||||
$this->links = FileUtils::readFlatDB($this->datastore, []);
|
$this->links = FileUtils::readFlatDB($this->datastore, []);
|
||||||
|
|
||||||
$toremove = array();
|
$toremove = [];
|
||||||
foreach ($this->links as $key => &$link) {
|
foreach ($this->links as $key => &$link) {
|
||||||
if (!$this->loggedIn && $link['private'] != 0) {
|
if (!$this->loggedIn && $link['private'] != 0) {
|
||||||
// Transition for not upgraded databases.
|
// Transition for not upgraded databases.
|
||||||
|
@ -414,7 +414,7 @@ public function filterDay($request)
|
||||||
* @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
|
* @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
|
||||||
*/
|
*/
|
||||||
public function filterSearch(
|
public function filterSearch(
|
||||||
$filterRequest = array(),
|
$filterRequest = [],
|
||||||
$casesensitive = false,
|
$casesensitive = false,
|
||||||
$visibility = 'all',
|
$visibility = 'all',
|
||||||
$untaggedonly = false
|
$untaggedonly = false
|
||||||
|
@ -512,7 +512,7 @@ public function renameTag($from, $to)
|
||||||
*/
|
*/
|
||||||
public function days()
|
public function days()
|
||||||
{
|
{
|
||||||
$linkDays = array();
|
$linkDays = [];
|
||||||
foreach ($this->links as $link) {
|
foreach ($this->links as $link) {
|
||||||
$linkDays[$link['created']->format('Ymd')] = 0;
|
$linkDays[$link['created']->format('Ymd')] = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ private function noFilter($visibility = 'all')
|
||||||
return $this->links;
|
return $this->links;
|
||||||
}
|
}
|
||||||
|
|
||||||
$out = array();
|
$out = [];
|
||||||
foreach ($this->links as $key => $value) {
|
foreach ($this->links as $key => $value) {
|
||||||
if ($value['private'] && $visibility === 'private') {
|
if ($value['private'] && $visibility === 'private') {
|
||||||
$out[$key] = $value;
|
$out[$key] = $value;
|
||||||
|
@ -143,7 +143,7 @@ private function noFilter($visibility = 'all')
|
||||||
*/
|
*/
|
||||||
private function filterSmallHash($smallHash)
|
private function filterSmallHash($smallHash)
|
||||||
{
|
{
|
||||||
$filtered = array();
|
$filtered = [];
|
||||||
foreach ($this->links as $key => $l) {
|
foreach ($this->links as $key => $l) {
|
||||||
if ($smallHash == $l['shorturl']) {
|
if ($smallHash == $l['shorturl']) {
|
||||||
// Yes, this is ugly and slow
|
// Yes, this is ugly and slow
|
||||||
|
@ -186,7 +186,7 @@ private function filterFulltext($searchterms, $visibility = 'all')
|
||||||
return $this->noFilter($visibility);
|
return $this->noFilter($visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filtered = array();
|
$filtered = [];
|
||||||
$search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
|
$search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
|
||||||
$exactRegex = '/"([^"]+)"/';
|
$exactRegex = '/"([^"]+)"/';
|
||||||
// Retrieve exact search terms.
|
// Retrieve exact search terms.
|
||||||
|
@ -198,8 +198,8 @@ private function filterFulltext($searchterms, $visibility = 'all')
|
||||||
$explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
|
$explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
|
||||||
|
|
||||||
// Filter excluding terms and update andSearch.
|
// Filter excluding terms and update andSearch.
|
||||||
$excludeSearch = array();
|
$excludeSearch = [];
|
||||||
$andSearch = array();
|
$andSearch = [];
|
||||||
foreach ($explodedSearchAnd as $needle) {
|
foreach ($explodedSearchAnd as $needle) {
|
||||||
if ($needle[0] == '-' && strlen($needle) > 1) {
|
if ($needle[0] == '-' && strlen($needle) > 1) {
|
||||||
$excludeSearch[] = substr($needle, 1);
|
$excludeSearch[] = substr($needle, 1);
|
||||||
|
@ -208,7 +208,7 @@ private function filterFulltext($searchterms, $visibility = 'all')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$keys = array('title', 'description', 'url', 'tags');
|
$keys = ['title', 'description', 'url', 'tags'];
|
||||||
|
|
||||||
// Iterate over every stored link.
|
// Iterate over every stored link.
|
||||||
foreach ($this->links as $id => $link) {
|
foreach ($this->links as $id => $link) {
|
||||||
|
@ -336,7 +336,7 @@ public function filterTags($tags, $casesensitive = false, $visibility = 'all')
|
||||||
}
|
}
|
||||||
|
|
||||||
// create resulting array
|
// create resulting array
|
||||||
$filtered = array();
|
$filtered = [];
|
||||||
|
|
||||||
// iterate over each link
|
// iterate over each link
|
||||||
foreach ($this->links as $key => $link) {
|
foreach ($this->links as $key => $link) {
|
||||||
|
@ -352,7 +352,7 @@ public function filterTags($tags, $casesensitive = false, $visibility = 'all')
|
||||||
$search = $link['tags']; // build search string, start with tags of current link
|
$search = $link['tags']; // build search string, start with tags of current link
|
||||||
if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) {
|
if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) {
|
||||||
// description given and at least one possible tag found
|
// description given and at least one possible tag found
|
||||||
$descTags = array();
|
$descTags = [];
|
||||||
// find all tags in the form of #tag in the description
|
// find all tags in the form of #tag in the description
|
||||||
preg_match_all(
|
preg_match_all(
|
||||||
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
|
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
|
||||||
|
@ -419,7 +419,7 @@ public function filterDay($day)
|
||||||
throw new Exception('Invalid date format');
|
throw new Exception('Invalid date format');
|
||||||
}
|
}
|
||||||
|
|
||||||
$filtered = array();
|
$filtered = [];
|
||||||
foreach ($this->links as $key => $l) {
|
foreach ($this->links as $key => $l) {
|
||||||
if ($l['created']->format('Ymd') == $day) {
|
if ($l['created']->format('Ymd') == $day) {
|
||||||
$filtered[$key] = $l;
|
$filtered[$key] = $l;
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
use ReflectionMethod;
|
use ReflectionMethod;
|
||||||
use Shaarli\ApplicationUtils;
|
|
||||||
use Shaarli\Bookmark\Bookmark;
|
use Shaarli\Bookmark\Bookmark;
|
||||||
use Shaarli\Bookmark\BookmarkArray;
|
use Shaarli\Bookmark\BookmarkArray;
|
||||||
use Shaarli\Bookmark\BookmarkFilter;
|
use Shaarli\Bookmark\BookmarkFilter;
|
||||||
|
@ -17,6 +16,7 @@
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
use Shaarli\Config\ConfigPhp;
|
use Shaarli\Config\ConfigPhp;
|
||||||
use Shaarli\Exceptions\IOException;
|
use Shaarli\Exceptions\IOException;
|
||||||
|
use Shaarli\Helper\ApplicationUtils;
|
||||||
use Shaarli\Thumbnailer;
|
use Shaarli\Thumbnailer;
|
||||||
use Shaarli\Updater\Exception\UpdaterException;
|
use Shaarli\Updater\Exception\UpdaterException;
|
||||||
|
|
||||||
|
@ -93,7 +93,7 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session
|
||||||
*/
|
*/
|
||||||
public function update()
|
public function update()
|
||||||
{
|
{
|
||||||
$updatesRan = array();
|
$updatesRan = [];
|
||||||
|
|
||||||
// If the user isn't logged in, exit without updating.
|
// If the user isn't logged in, exit without updating.
|
||||||
if ($this->isLoggedIn !== true) {
|
if ($this->isLoggedIn !== true) {
|
||||||
|
@ -106,7 +106,8 @@ public function update()
|
||||||
|
|
||||||
foreach ($this->methods as $method) {
|
foreach ($this->methods as $method) {
|
||||||
// Not an update method or already done, pass.
|
// Not an update method or already done, pass.
|
||||||
if (!startsWith($method->getName(), 'updateMethod')
|
if (
|
||||||
|
!startsWith($method->getName(), 'updateMethod')
|
||||||
|| in_array($method->getName(), $this->doneUpdates)
|
|| in_array($method->getName(), $this->doneUpdates)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -189,7 +190,7 @@ public function updateMethodConfigToJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set sub config keys (config and plugins)
|
// Set sub config keys (config and plugins)
|
||||||
$subConfig = array('config', 'plugins');
|
$subConfig = ['config', 'plugins'];
|
||||||
foreach ($subConfig as $sub) {
|
foreach ($subConfig as $sub) {
|
||||||
foreach ($oldConfig[$sub] as $key => $value) {
|
foreach ($oldConfig[$sub] as $key => $value) {
|
||||||
if (isset($legacyMap[$sub . '.' . $key])) {
|
if (isset($legacyMap[$sub . '.' . $key])) {
|
||||||
|
@ -259,7 +260,7 @@ public function updateMethodDatastoreIds()
|
||||||
$save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
|
$save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
|
||||||
copy($this->conf->get('resource.datastore'), $save);
|
copy($this->conf->get('resource.datastore'), $save);
|
||||||
|
|
||||||
$links = array();
|
$links = [];
|
||||||
foreach ($this->linkDB as $offset => $value) {
|
foreach ($this->linkDB as $offset => $value) {
|
||||||
$links[] = $value;
|
$links[] = $value;
|
||||||
unset($this->linkDB[$offset]);
|
unset($this->linkDB[$offset]);
|
||||||
|
@ -498,7 +499,8 @@ public function updateMethodVisibilitySession()
|
||||||
*/
|
*/
|
||||||
public function updateMethodDownloadSizeAndTimeoutConf()
|
public function updateMethodDownloadSizeAndTimeoutConf()
|
||||||
{
|
{
|
||||||
if ($this->conf->exists('general.download_max_size')
|
if (
|
||||||
|
$this->conf->exists('general.download_max_size')
|
||||||
&& $this->conf->exists('general.download_timeout')
|
&& $this->conf->exists('general.download_timeout')
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -585,7 +587,7 @@ public function updateMethodMigrateDatabase()
|
||||||
|
|
||||||
$linksArray = new BookmarkArray();
|
$linksArray = new BookmarkArray();
|
||||||
foreach ($this->linkDB as $key => $link) {
|
foreach ($this->linkDB as $key => $link) {
|
||||||
$linksArray[$key] = (new Bookmark())->fromArray($link);
|
$linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' '));
|
||||||
}
|
}
|
||||||
$linksIo = new BookmarkIO($this->conf);
|
$linksIo = new BookmarkIO($this->conf);
|
||||||
$linksIo->write($linksArray);
|
$linksIo->write($linksArray);
|
||||||
|
|
|
@ -59,11 +59,11 @@ public function filterAndFormat(
|
||||||
$indexUrl
|
$indexUrl
|
||||||
) {
|
) {
|
||||||
// see tpl/export.html for possible values
|
// see tpl/export.html for possible values
|
||||||
if (!in_array($selection, array('all', 'public', 'private'))) {
|
if (!in_array($selection, ['all', 'public', 'private'])) {
|
||||||
throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
|
throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
$bookmarkLinks = array();
|
$bookmarkLinks = [];
|
||||||
foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
|
foreach ($this->bookmarkService->search([], $selection) as $bookmark) {
|
||||||
$link = $formatter->format($bookmark);
|
$link = $formatter->format($bookmark);
|
||||||
$link['taglist'] = implode(',', $bookmark->getTags());
|
$link['taglist'] = implode(',', $bookmark->getTags());
|
||||||
|
@ -101,11 +101,11 @@ public function import($post, UploadedFileInterface $file)
|
||||||
|
|
||||||
// Add tags to all imported bookmarks?
|
// Add tags to all imported bookmarks?
|
||||||
if (empty($post['default_tags'])) {
|
if (empty($post['default_tags'])) {
|
||||||
$defaultTags = array();
|
$defaultTags = [];
|
||||||
} else {
|
} else {
|
||||||
$defaultTags = preg_split(
|
$defaultTags = tags_str2array(
|
||||||
'/[\s,]+/',
|
escape($post['default_tags']),
|
||||||
escape($post['default_tags'])
|
$this->conf->get('general.tags_separator', ' ')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +171,7 @@ public function import($post, UploadedFileInterface $file)
|
||||||
$link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
|
$link->setUrl($bkm['uri'], $this->conf->get('security.allowed_protocols'));
|
||||||
$link->setDescription($bkm['note']);
|
$link->setDescription($bkm['note']);
|
||||||
$link->setPrivate($private);
|
$link->setPrivate($private);
|
||||||
$link->setTagsString($bkm['tags']);
|
$link->setTags($bkm['tags']);
|
||||||
|
|
||||||
$this->bookmarkService->addOrSet($link, false);
|
$this->bookmarkService->addOrSet($link, false);
|
||||||
$importCount++;
|
$importCount++;
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Plugin;
|
namespace Shaarli\Plugin;
|
||||||
|
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
|
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
|
||||||
|
use Shaarli\Plugin\Exception\PluginInvalidRouteException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class PluginManager
|
* Class PluginManager
|
||||||
|
@ -23,7 +25,15 @@ class PluginManager
|
||||||
*
|
*
|
||||||
* @var array $loadedPlugins
|
* @var array $loadedPlugins
|
||||||
*/
|
*/
|
||||||
private $loadedPlugins = array();
|
private $loadedPlugins = [];
|
||||||
|
|
||||||
|
/** @var array List of registered routes. Contains keys:
|
||||||
|
* - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
|
||||||
|
* - `route` (path): without prefix, e.g. `/up/{variable}`
|
||||||
|
* It will be later prefixed by `/plugin/<plugin name>/`.
|
||||||
|
* - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
|
||||||
|
*/
|
||||||
|
protected $registeredRoutes = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ConfigManager Configuration Manager instance.
|
* @var ConfigManager Configuration Manager instance.
|
||||||
|
@ -57,7 +67,7 @@ class PluginManager
|
||||||
public function __construct(&$conf)
|
public function __construct(&$conf)
|
||||||
{
|
{
|
||||||
$this->conf = $conf;
|
$this->conf = $conf;
|
||||||
$this->errors = array();
|
$this->errors = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,6 +95,9 @@ public function load($authorizedPlugins)
|
||||||
$this->loadPlugin($dirs[$index], $plugin);
|
$this->loadPlugin($dirs[$index], $plugin);
|
||||||
} catch (PluginFileNotFoundException $e) {
|
} catch (PluginFileNotFoundException $e) {
|
||||||
error_log($e->getMessage());
|
error_log($e->getMessage());
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
|
||||||
|
$this->errors = array_unique(array_merge($this->errors, [$error]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +111,7 @@ public function load($authorizedPlugins)
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function executeHooks($hook, &$data, $params = array())
|
public function executeHooks($hook, &$data, $params = [])
|
||||||
{
|
{
|
||||||
$metadataParameters = [
|
$metadataParameters = [
|
||||||
'target' => '_PAGE_',
|
'target' => '_PAGE_',
|
||||||
|
@ -165,6 +178,22 @@ private function loadPlugin($dir, $pluginName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$registerRouteFunction = $pluginName . '_register_routes';
|
||||||
|
$routes = null;
|
||||||
|
if (function_exists($registerRouteFunction)) {
|
||||||
|
$routes = call_user_func($registerRouteFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($routes !== null) {
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
if (static::validateRouteRegistration($route)) {
|
||||||
|
$this->registeredRoutes[$pluginName][] = $route;
|
||||||
|
} else {
|
||||||
|
throw new PluginInvalidRouteException($pluginName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$this->loadedPlugins[] = $pluginName;
|
$this->loadedPlugins[] = $pluginName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +225,7 @@ public function buildHookName($hook, $pluginName)
|
||||||
*/
|
*/
|
||||||
public function getPluginsMeta()
|
public function getPluginsMeta()
|
||||||
{
|
{
|
||||||
$metaData = array();
|
$metaData = [];
|
||||||
$dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
|
$dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
|
||||||
|
|
||||||
// Browse all plugin directories.
|
// Browse all plugin directories.
|
||||||
|
@ -217,9 +246,9 @@ public function getPluginsMeta()
|
||||||
if (isset($metaData[$plugin]['parameters'])) {
|
if (isset($metaData[$plugin]['parameters'])) {
|
||||||
$params = explode(';', $metaData[$plugin]['parameters']);
|
$params = explode(';', $metaData[$plugin]['parameters']);
|
||||||
} else {
|
} else {
|
||||||
$params = array();
|
$params = [];
|
||||||
}
|
}
|
||||||
$metaData[$plugin]['parameters'] = array();
|
$metaData[$plugin]['parameters'] = [];
|
||||||
foreach ($params as $param) {
|
foreach ($params as $param) {
|
||||||
if (empty($param)) {
|
if (empty($param)) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -236,6 +265,14 @@ public function getPluginsMeta()
|
||||||
return $metaData;
|
return $metaData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array List of registered custom routes by plugins.
|
||||||
|
*/
|
||||||
|
public function getRegisteredRoutes(): array
|
||||||
|
{
|
||||||
|
return $this->registeredRoutes;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the list of encountered errors.
|
* Return the list of encountered errors.
|
||||||
*
|
*
|
||||||
|
@ -245,4 +282,32 @@ public function getErrors()
|
||||||
{
|
{
|
||||||
return $this->errors;
|
return $this->errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether provided input is valid to register a new route.
|
||||||
|
* It must contain keys `method`, `route`, `callable` (all strings).
|
||||||
|
*
|
||||||
|
* @param string[] $input
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected static function validateRouteRegistration(array $input): bool
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!array_key_exists('method', $input)
|
||||||
|
|| !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('callable', $input)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Plugin\Exception;
|
namespace Shaarli\Plugin\Exception;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
26
application/plugin/exception/PluginInvalidRouteException.php
Normal file
26
application/plugin/exception/PluginInvalidRouteException.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shaarli\Plugin\Exception;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class PluginFileNotFoundException
|
||||||
|
*
|
||||||
|
* Raise when plugin files can't be found.
|
||||||
|
*/
|
||||||
|
class PluginInvalidRouteException extends Exception
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Construct exception with plugin name.
|
||||||
|
* Generate message.
|
||||||
|
*
|
||||||
|
* @param string $pluginName name of the plugin not found
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->message = 'trying to register invalid route.';
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,11 @@
|
||||||
namespace Shaarli\Render;
|
namespace Shaarli\Render;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use exceptions\MissingBasePathException;
|
use Psr\Log\LoggerInterface;
|
||||||
use RainTPL;
|
use RainTPL;
|
||||||
use Shaarli\ApplicationUtils;
|
|
||||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
|
use Shaarli\Helper\ApplicationUtils;
|
||||||
use Shaarli\Security\SessionManager;
|
use Shaarli\Security\SessionManager;
|
||||||
use Shaarli\Thumbnailer;
|
use Shaarli\Thumbnailer;
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ class PageBuilder
|
||||||
*/
|
*/
|
||||||
protected $session;
|
protected $session;
|
||||||
|
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var BookmarkServiceInterface $bookmarkService instance.
|
* @var BookmarkServiceInterface $bookmarkService instance.
|
||||||
*/
|
*/
|
||||||
|
@ -54,17 +57,25 @@ class PageBuilder
|
||||||
* PageBuilder constructor.
|
* PageBuilder constructor.
|
||||||
* $tpl is initialized at false for lazy loading.
|
* $tpl is initialized at false for lazy loading.
|
||||||
*
|
*
|
||||||
* @param ConfigManager $conf Configuration Manager instance (reference).
|
* @param ConfigManager $conf Configuration Manager instance (reference).
|
||||||
* @param array $session $_SESSION array
|
* @param array $session $_SESSION array
|
||||||
* @param BookmarkServiceInterface $linkDB instance.
|
* @param LoggerInterface $logger
|
||||||
* @param string $token Session token
|
* @param null $linkDB instance.
|
||||||
* @param bool $isLoggedIn
|
* @param null $token Session token
|
||||||
|
* @param bool $isLoggedIn
|
||||||
*/
|
*/
|
||||||
public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
|
public function __construct(
|
||||||
{
|
ConfigManager &$conf,
|
||||||
|
array $session,
|
||||||
|
LoggerInterface $logger,
|
||||||
|
$linkDB = null,
|
||||||
|
$token = null,
|
||||||
|
$isLoggedIn = false
|
||||||
|
) {
|
||||||
$this->tpl = false;
|
$this->tpl = false;
|
||||||
$this->conf = $conf;
|
$this->conf = $conf;
|
||||||
$this->session = $session;
|
$this->session = $session;
|
||||||
|
$this->logger = $logger;
|
||||||
$this->bookmarkService = $linkDB;
|
$this->bookmarkService = $linkDB;
|
||||||
$this->token = $token;
|
$this->token = $token;
|
||||||
$this->isLoggedIn = $isLoggedIn;
|
$this->isLoggedIn = $isLoggedIn;
|
||||||
|
@ -98,7 +109,7 @@ private function initialize()
|
||||||
$this->tpl->assign('newVersion', escape($version));
|
$this->tpl->assign('newVersion', escape($version));
|
||||||
$this->tpl->assign('versionError', '');
|
$this->tpl->assign('versionError', '');
|
||||||
} catch (Exception $exc) {
|
} catch (Exception $exc) {
|
||||||
logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage());
|
$this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER)));
|
||||||
$this->tpl->assign('newVersion', '');
|
$this->tpl->assign('newVersion', '');
|
||||||
$this->tpl->assign('versionError', escape($exc->getMessage()));
|
$this->tpl->assign('versionError', escape($exc->getMessage()));
|
||||||
}
|
}
|
||||||
|
@ -149,7 +160,8 @@ private function initialize()
|
||||||
|
|
||||||
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
|
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
|
||||||
|
|
||||||
$this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE']);
|
$this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
|
||||||
|
$this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' '));
|
||||||
|
|
||||||
// To be removed with a proper theme configuration.
|
// To be removed with a proper theme configuration.
|
||||||
$this->tpl->assign('conf', $this->conf);
|
$this->tpl->assign('conf', $this->conf);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace Shaarli\Render;
|
namespace Shaarli\Render;
|
||||||
|
|
||||||
|
use DatePeriod;
|
||||||
use Shaarli\Feed\CachedPage;
|
use Shaarli\Feed\CachedPage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -49,12 +50,21 @@ public function invalidateCaches(): void
|
||||||
$this->purgeCachedPages();
|
$this->purgeCachedPages();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCachePage(string $pageUrl): CachedPage
|
/**
|
||||||
|
* Get CachedPage instance for provided URL.
|
||||||
|
*
|
||||||
|
* @param string $pageUrl
|
||||||
|
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
|
||||||
|
*
|
||||||
|
* @return CachedPage
|
||||||
|
*/
|
||||||
|
public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
|
||||||
{
|
{
|
||||||
return new CachedPage(
|
return new CachedPage(
|
||||||
$this->pageCacheDir,
|
$this->pageCacheDir,
|
||||||
$pageUrl,
|
$pageUrl,
|
||||||
false === $this->isLoggedIn
|
false === $this->isLoggedIn,
|
||||||
|
$validityPeriod
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ interface TemplatePage
|
||||||
public const DAILY = 'daily';
|
public const DAILY = 'daily';
|
||||||
public const DAILY_RSS = 'dailyrss';
|
public const DAILY_RSS = 'dailyrss';
|
||||||
public const EDIT_LINK = 'editlink';
|
public const EDIT_LINK = 'editlink';
|
||||||
|
public const EDIT_LINK_BATCH = 'editlink.batch';
|
||||||
public const ERROR = 'error';
|
public const ERROR = 'error';
|
||||||
public const EXPORT = 'export';
|
public const EXPORT = 'export';
|
||||||
public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
|
public const NETSCAPE_EXPORT_BOOKMARKS = 'export.bookmarks';
|
||||||
|
|
|
@ -23,10 +23,10 @@ class ThemeUtils
|
||||||
public static function getThemes($tplDir)
|
public static function getThemes($tplDir)
|
||||||
{
|
{
|
||||||
$tplDir = rtrim($tplDir, '/');
|
$tplDir = rtrim($tplDir, '/');
|
||||||
$allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
|
$allTheme = glob($tplDir . '/*', GLOB_ONLYDIR);
|
||||||
$themes = [];
|
$themes = [];
|
||||||
foreach ($allTheme as $value) {
|
foreach ($allTheme as $value) {
|
||||||
$themes[] = str_replace($tplDir.'/', '', $value);
|
$themes[] = str_replace($tplDir . '/', '', $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $themes;
|
return $themes;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace Shaarli\Security;
|
namespace Shaarli\Security;
|
||||||
|
|
||||||
use Shaarli\FileUtils;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Shaarli\Helper\FileUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BanManager
|
* Class BanManager
|
||||||
|
@ -28,8 +28,8 @@ class BanManager
|
||||||
/** @var string Path to the file containing IP bans and failures */
|
/** @var string Path to the file containing IP bans and failures */
|
||||||
protected $banFile;
|
protected $banFile;
|
||||||
|
|
||||||
/** @var string Path to the log file, used to log bans */
|
/** @var LoggerInterface Path to the log file, used to log bans */
|
||||||
protected $logFile;
|
protected $logger;
|
||||||
|
|
||||||
/** @var array List of IP with their associated number of failed attempts */
|
/** @var array List of IP with their associated number of failed attempts */
|
||||||
protected $failures = [];
|
protected $failures = [];
|
||||||
|
@ -40,18 +40,20 @@ class BanManager
|
||||||
/**
|
/**
|
||||||
* BanManager constructor.
|
* BanManager constructor.
|
||||||
*
|
*
|
||||||
* @param array $trustedProxies List of allowed proxies IP
|
* @param array $trustedProxies List of allowed proxies IP
|
||||||
* @param int $nbAttempts Number of allowed failed attempt before the ban
|
* @param int $nbAttempts Number of allowed failed attempt before the ban
|
||||||
* @param int $banDuration Ban duration in seconds
|
* @param int $banDuration Ban duration in seconds
|
||||||
* @param string $banFile Path to the file containing IP bans and failures
|
* @param string $banFile Path to the file containing IP bans and failures
|
||||||
* @param string $logFile Path to the log file, used to log bans
|
* @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory
|
||||||
*/
|
*/
|
||||||
public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) {
|
public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger)
|
||||||
|
{
|
||||||
$this->trustedProxies = $trustedProxies;
|
$this->trustedProxies = $trustedProxies;
|
||||||
$this->nbAttempts = $nbAttempts;
|
$this->nbAttempts = $nbAttempts;
|
||||||
$this->banDuration = $banDuration;
|
$this->banDuration = $banDuration;
|
||||||
$this->banFile = $banFile;
|
$this->banFile = $banFile;
|
||||||
$this->logFile = $logFile;
|
$this->logger = $logger;
|
||||||
|
|
||||||
$this->readBanFile();
|
$this->readBanFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,11 +80,7 @@ public function handleFailedAttempt($server)
|
||||||
|
|
||||||
if ($this->failures[$ip] >= $this->nbAttempts) {
|
if ($this->failures[$ip] >= $this->nbAttempts) {
|
||||||
$this->bans[$ip] = time() + $this->banDuration;
|
$this->bans[$ip] = time() + $this->banDuration;
|
||||||
logm(
|
$this->logger->info(format_log('IP address banned from login: ' . $ip, $ip));
|
||||||
$this->logFile,
|
|
||||||
$server['REMOTE_ADDR'],
|
|
||||||
'IP address banned from login: '. $ip
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$this->writeBanFile();
|
$this->writeBanFile();
|
||||||
}
|
}
|
||||||
|
@ -138,7 +136,7 @@ public function isBanned($server)
|
||||||
unset($this->failures[$ip]);
|
unset($this->failures[$ip]);
|
||||||
}
|
}
|
||||||
unset($this->bans[$ip]);
|
unset($this->bans[$ip]);
|
||||||
logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip);
|
$this->logger->info(format_log('Ban lifted for: ' . $ip, $ip));
|
||||||
|
|
||||||
$this->writeBanFile();
|
$this->writeBanFile();
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Security;
|
namespace Shaarli\Security;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,26 +33,30 @@ class LoginManager
|
||||||
protected $staySignedInToken = '';
|
protected $staySignedInToken = '';
|
||||||
/** @var CookieManager */
|
/** @var CookieManager */
|
||||||
protected $cookieManager;
|
protected $cookieManager;
|
||||||
|
/** @var LoggerInterface */
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*
|
*
|
||||||
* @param ConfigManager $configManager Configuration Manager instance
|
* @param ConfigManager $configManager Configuration Manager instance
|
||||||
* @param SessionManager $sessionManager SessionManager instance
|
* @param SessionManager $sessionManager SessionManager instance
|
||||||
* @param CookieManager $cookieManager CookieManager instance
|
* @param CookieManager $cookieManager CookieManager instance
|
||||||
|
* @param BanManager $banManager
|
||||||
|
* @param LoggerInterface $logger Used to log login attempts
|
||||||
*/
|
*/
|
||||||
public function __construct($configManager, $sessionManager, $cookieManager)
|
public function __construct(
|
||||||
{
|
ConfigManager $configManager,
|
||||||
|
SessionManager $sessionManager,
|
||||||
|
CookieManager $cookieManager,
|
||||||
|
BanManager $banManager,
|
||||||
|
LoggerInterface $logger
|
||||||
|
) {
|
||||||
$this->configManager = $configManager;
|
$this->configManager = $configManager;
|
||||||
$this->sessionManager = $sessionManager;
|
$this->sessionManager = $sessionManager;
|
||||||
$this->cookieManager = $cookieManager;
|
$this->cookieManager = $cookieManager;
|
||||||
$this->banManager = new BanManager(
|
$this->banManager = $banManager;
|
||||||
$this->configManager->get('security.trusted_proxies', []),
|
$this->logger = $logger;
|
||||||
$this->configManager->get('security.ban_after'),
|
|
||||||
$this->configManager->get('security.ban_duration'),
|
|
||||||
$this->configManager->get('resource.ban_file', 'data/ipbans.php'),
|
|
||||||
$this->configManager->get('resource.log')
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($this->configManager->get('security.open_shaarli') === true) {
|
if ($this->configManager->get('security.open_shaarli') === true) {
|
||||||
$this->openShaarli = true;
|
$this->openShaarli = true;
|
||||||
|
@ -101,7 +107,8 @@ public function checkLoginState($clientIpId)
|
||||||
// The user client has a valid stay-signed-in cookie
|
// The user client has a valid stay-signed-in cookie
|
||||||
// Session information is updated with the current client information
|
// Session information is updated with the current client information
|
||||||
$this->sessionManager->storeLoginInfo($clientIpId);
|
$this->sessionManager->storeLoginInfo($clientIpId);
|
||||||
} elseif ($this->sessionManager->hasSessionExpired()
|
} elseif (
|
||||||
|
$this->sessionManager->hasSessionExpired()
|
||||||
|| $this->sessionManager->hasClientIpChanged($clientIpId)
|
|| $this->sessionManager->hasClientIpChanged($clientIpId)
|
||||||
) {
|
) {
|
||||||
$this->sessionManager->logout();
|
$this->sessionManager->logout();
|
||||||
|
@ -129,48 +136,35 @@ public function isLoggedIn(): bool
|
||||||
/**
|
/**
|
||||||
* Check user credentials are valid
|
* Check user credentials are valid
|
||||||
*
|
*
|
||||||
* @param string $remoteIp Remote client IP address
|
|
||||||
* @param string $clientIpId Client IP address identifier
|
* @param string $clientIpId Client IP address identifier
|
||||||
* @param string $login Username
|
* @param string $login Username
|
||||||
* @param string $password Password
|
* @param string $password Password
|
||||||
*
|
*
|
||||||
* @return bool true if the provided credentials are valid, false otherwise
|
* @return bool true if the provided credentials are valid, false otherwise
|
||||||
*/
|
*/
|
||||||
public function checkCredentials($remoteIp, $clientIpId, $login, $password)
|
public function checkCredentials($clientIpId, $login, $password)
|
||||||
{
|
{
|
||||||
// Check login matches config
|
|
||||||
if ($login !== $this->configManager->get('credentials.login')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check credentials
|
// Check credentials
|
||||||
try {
|
try {
|
||||||
$useLdapLogin = !empty($this->configManager->get('ldap.host'));
|
$useLdapLogin = !empty($this->configManager->get('ldap.host'));
|
||||||
if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
|
if (
|
||||||
|| (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
|
$login === $this->configManager->get('credentials.login')
|
||||||
|
&& (
|
||||||
|
(false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
|
||||||
|
|| (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
$this->sessionManager->storeLoginInfo($clientIpId);
|
$this->sessionManager->storeLoginInfo($clientIpId);
|
||||||
logm(
|
$this->logger->info(format_log('Login successful', $clientIpId));
|
||||||
$this->configManager->get('resource.log'),
|
|
||||||
$remoteIp,
|
return true;
|
||||||
'Login successful'
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
} catch (Exception $exception) {
|
||||||
catch(Exception $exception) {
|
$this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId));
|
||||||
logm(
|
|
||||||
$this->configManager->get('resource.log'),
|
|
||||||
$remoteIp,
|
|
||||||
'Exception while checking credentials: ' . $exception
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logm(
|
$this->logger->info(format_log('Login failed for user ' . $login, $clientIpId));
|
||||||
$this->configManager->get('resource.log'),
|
|
||||||
$remoteIp,
|
|
||||||
'Login failed for user ' . $login
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +177,8 @@ public function checkCredentials($remoteIp, $clientIpId, $login, $password)
|
||||||
*
|
*
|
||||||
* @return bool true if the provided credentials are valid, false otherwise
|
* @return bool true if the provided credentials are valid, false otherwise
|
||||||
*/
|
*/
|
||||||
public function checkCredentialsFromLocalConfig($login, $password) {
|
public function checkCredentialsFromLocalConfig($login, $password)
|
||||||
|
{
|
||||||
$hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
|
$hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
|
||||||
|
|
||||||
return $login == $this->configManager->get('credentials.login')
|
return $login == $this->configManager->get('credentials.login')
|
||||||
|
@ -202,14 +197,14 @@ public function checkCredentialsFromLocalConfig($login, $password) {
|
||||||
*/
|
*/
|
||||||
public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null)
|
public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null)
|
||||||
{
|
{
|
||||||
$connect = $connect ?? function($host) {
|
$connect = $connect ?? function ($host) {
|
||||||
$resource = ldap_connect($host);
|
$resource = ldap_connect($host);
|
||||||
|
|
||||||
ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3);
|
ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3);
|
||||||
|
|
||||||
return $resource;
|
return $resource;
|
||||||
};
|
};
|
||||||
$bind = $bind ?? function($handle, $dn, $password) {
|
$bind = $bind ?? function ($handle, $dn, $password) {
|
||||||
return ldap_bind($handle, $dn, $password);
|
return ldap_bind($handle, $dn, $password);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Shaarli\Security;
|
namespace Shaarli\Security;
|
||||||
|
|
||||||
use Shaarli\Config\ConfigManager;
|
use Shaarli\Config\ConfigManager;
|
||||||
|
@ -79,7 +80,7 @@ public function setStaySignedIn($staySignedIn)
|
||||||
*/
|
*/
|
||||||
public function generateToken()
|
public function generateToken()
|
||||||
{
|
{
|
||||||
$token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
|
$token = sha1(uniqid('', true) . '_' . mt_rand() . $this->conf->get('credentials.salt'));
|
||||||
$this->session['tokens'][$token] = 1;
|
$this->session['tokens'][$token] = 1;
|
||||||
return $token;
|
return $token;
|
||||||
}
|
}
|
||||||
|
@ -293,9 +294,12 @@ public function start(): bool
|
||||||
return session_start();
|
return session_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function cookieParameters(int $lifeTime, string $path, string $domain): bool
|
/**
|
||||||
|
* Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2.
|
||||||
|
*/
|
||||||
|
public function cookieParameters(int $lifeTime, string $path, string $domain): void
|
||||||
{
|
{
|
||||||
return session_set_cookie_params($lifeTime, $path, $domain);
|
session_set_cookie_params($lifeTime, $path, $domain);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function regenerateId(bool $deleteOldSession = false): bool
|
public function regenerateId(bool $deleteOldSession = false): bool
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue