Merge branch 'master' into PHRAS-2184-list-manager-email-domain-filter

This commit is contained in:
Nicolas Maillat
2020-04-21 14:41:14 +02:00
committed by GitHub
315 changed files with 26274 additions and 7029 deletions

View File

@@ -107,6 +107,53 @@ jobs:
- store_artifacts:
path: /tmp/circleci-test-results
build_phraseanet-fpm:
machine:
image: ubuntu-1604:201903-01
docker_layer_caching: true
working_directory: ~/alchemy-fr/Phraseanet
steps:
- checkout
- aws-ecr/ecr-login:
region: AWS_DEFAULT_REGION
- run: docker build --target phraseanet-fpm -t ${AWS_ACCOUNT_URL}/phraseanet-fpm:${CIRCLE_BRANCH} .
- aws-ecr/push-image:
account-url: AWS_ACCOUNT_URL
repo: "phraseanet-fpm"
tag: "${CIRCLE_BRANCH}"
build_phraseanet-worker:
machine:
image: ubuntu-1604:201903-01
docker_layer_caching: true
working_directory: ~/alchemy-fr/Phraseanet
steps:
- checkout
- aws-ecr/ecr-login:
region: AWS_DEFAULT_REGION
- run: docker build --target phraseanet-worker -t ${AWS_ACCOUNT_URL}/phraseanet-worker:${CIRCLE_BRANCH} .
- aws-ecr/push-image:
account-url: AWS_ACCOUNT_URL
repo: "phraseanet-worker"
tag: "${CIRCLE_BRANCH}"
build_phraseanet-nginx:
machine:
image: ubuntu-1604:201903-01
docker_layer_caching: true
working_directory: ~/alchemy-fr/Phraseanet
steps:
- checkout
- aws-ecr/ecr-login:
region: AWS_DEFAULT_REGION
- run: docker build --target phraseanet-nginx -t ${AWS_ACCOUNT_URL}/phraseanet-nginx:${CIRCLE_BRANCH} .
- aws-ecr/push-image:
account-url: AWS_ACCOUNT_URL
repo: "phraseanet-nginx"
tag: "${CIRCLE_BRANCH}"
workflows:
version: 2
oldfashion:
@@ -114,36 +161,9 @@ workflows:
- build
newfashion:
jobs:
- aws-ecr/build_and_push_image:
account-url: AWS_ACCOUNT_URL
aws-access-key-id: AWS_ACCESS_KEY_ID
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
- build_phraseanet-fpm:
context: "AWS London"
create-repo: true
dockerfile: Dockerfile
extra-build-args: "--target phraseanet-fpm"
region: AWS_DEFAULT_REGION
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet"
tag: "alpha-0.1"
- aws-ecr/build_and_push_image:
account-url: AWS_ACCOUNT_URL
aws-access-key-id: AWS_ACCESS_KEY_ID
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
- build_phraseanet-worker:
context: "AWS London"
create-repo: true
dockerfile: Dockerfile
extra-build-args: "--target phraseanet-nginx"
region: AWS_DEFAULT_REGION
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet-nginx"
tag: "alpha-0.1"
- aws-ecr/build_and_push_image:
account-url: AWS_ACCOUNT_URL
aws-access-key-id: AWS_ACCESS_KEY_ID
aws-secret-access-key: AWS_SECRET_ACCESS_KEY
- build_phraseanet-nginx:
context: "AWS London"
create-repo: true
dockerfile: Dockerfile
extra-build-args: "--target phraseanet-worker"
region: AWS_DEFAULT_REGION
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet"
tag: "alpha-0.1"

36
.dockerignore Normal file
View File

@@ -0,0 +1,36 @@
.idea
.settings
/*.md
/.circleci
/.dockerignore
/.env
/.env.*
/env.*
/.git
/.gitignore
/.travis.yml
/AUTHORS
/CONTRIBUTORS
/Dockerfile
/LICENSE
/bin
!/bin/console
!/bin/developer
!/bin/setup
/cache
/config/configuration.yml
/config/configuration-compiled.php
!/config/configuration.sample.yml
/datas
/docker-compose.*
/logs
/node_modules
/plugins
/tmp
/vendor
/volumes
/www/assets
/www/custom
/www/plugins
/www/thumbnails
npm-debug.log

83
.env Normal file
View File

@@ -0,0 +1,83 @@
PHRASEANET_PROJECT_NAME=Phraseanet
# Registry from where you pull Docker images
PHRASEANET_DOCKER_REGISTRY=local
# Tag of the Docker images
PHRASEANET_DOCKER_TAG=latest
# APPLICATION PORT
PHRASEANET_APP_PORT=8082
# RabbitMQ configuration
RABBITMQ_DEFAULT_USER=alchemy
RABBITMQ_DEFAULT_PASS=vdh4dpe5Wy3R
RABBITMQ_MANAGEMENT_PORT=10811
# Mysql configuration
MYSQL_ROOT_PASSWORD=root
SERVER_NAME=phraseanet-docker
# --------------- APPLICATION CONFIGURATION --------------------
# Max upload size
MAX_BODY_SIZE=2G
# Max input var
MAX_INPUT_VARS=12000
# Enable opcache ? (0/1)
OPCACHE_ENABLED=1
# session cache limiter (off/on)
SESSION_CACHE_LIMITER=off
# PHP LOG LEVEL : Possible Values: alert, error, warning, notice, debug
PHP_LOG_LEVEL=warning
# --------------- APPLICATION STARTUP CONFIGURATION --------------------
# These variables are only used if the configuration.yml file is not present, in order to automate the installation procedure
# set here the first user / email couple
INSTALL_ACCOUNT_EMAIL=admin@alchemy.fr
INSTALL_ACCOUNT_PASSWORD=iJRqXU0MwbyJewQLBbra6IWHsWly
# Database parameters
INSTALL_DB_HOST=db
INSTALL_DB_PORT=3306
INSTALL_DB_USER=root
INSTALL_DB_PASSWORD=root
INSTALL_DB_TEMPLATE=en-simple
INSTALL_APPBOX=ab_master
INSTALL_DATABOX=db_databox1
INSTALL_SERVER_NAME=localhost
# Mysql max allowed packet
MYSQL_MAX_ALLOWED_PACKET=16M
# --- DEV purpose ---
# PhpMyAdmin port
PHRASEANET_PHPMYADMIN_PORT=8089
# Xdebug
XDEBUG_ENABLED=1
XDEBUG_PROFILER_ENABLED=0
IDE_KEY=PHPSTORM
PHRASEANET_SUBNET_IPS=172.32.0.0/16
XDEBUG_REMOTE_HOST=172.32.0.1
PHP_IDE_CONFIG=serverName=docker-server-phraseanet
# Volumes location for dev
PHRASEANET_CONFIG_DIR=./config
PHRASEANET_LOGS_DIR=./logs
PHRASEANET_DATA_DIR=./datas
PHRASEANET_DB_DIR=./volumes/db
PHRASEANET_ELASTICSEARCH_DIR=./volumes/elasticsearch
PHRASEANET_THUMBNAILS_DIR=./www/thumbnails
PHRASEANET_CUSTOM_DIR=./www/custom
PHRASEANET_TMP_DIR=./tmp
PHRASEANET_CACHE_DIR=./cache
# For dev who don't have SSH_AUTH_SOCK (avoid an empty volume name)
SSH_AUTH_SOCK=/dev/null
# Plugin support
PHRASEANET_PLUGINS=
PHRASEANET_SSH_PRIVATE_KEY=

4
.gitignore vendored
View File

@@ -71,3 +71,7 @@ playbook.retry
npm-debug.log
/Phrasea_datas
.env.*
env.local
/volumes

View File

@@ -1,12 +1,21 @@
FROM php:7.0-fpm-stretch as builder
#########################################################################
# This image contains every build tools that will be used by the builder and
# the app images (usefull in dev mode)
#########################################################################
RUN apt-get update \
FROM php:7.0-fpm-stretch as phraseanet-system
ENV FFMPEG_VERSION=4.2.2
RUN echo "deb http://deb.debian.org/debian stretch main non-free" > /etc/apt/sources.list \
&& apt-get update \
&& apt-get install -y \
apt-transport-https \
ca-certificates \
gnupg2 \
&& apt-get update \
&& apt-get install -y --no-install-recommends zlib1g-dev \
&& apt-get install -y --no-install-recommends \
zlib1g-dev \
git \
ghostscript \
gpac \
@@ -23,142 +32,194 @@ RUN apt-get update \
libxslt-dev \
libzmq3-dev \
locales \
gettext \
mcrypt \
swftools \
unoconv \
unzip \
xpdf \
libreoffice-base-core \
libreoffice-impress \
libreoffice-calc \
libreoffice-math \
libreoffice-writer \
libreoffice-pdfimport \
# FFmpeg
yasm \
libvorbis-dev \
texi2html \
nasm \
zlib1g-dev \
libx264-dev \
libfdk-aac-dev \
libopus-dev \
libvpx-dev \
libmp3lame-dev \
libogg-dev \
libopencore-amrnb-dev \
libopencore-amrwb-dev \
libdc1394-22-dev \
libx11-dev \
libswscale-dev \
libpostproc-dev \
libxvidcore-dev \
libtheora-dev \
libgsm1-dev \
libfreetype6-dev \
# End FFmpeg
&& update-locale "LANG=fr_FR.UTF-8 UTF-8" \
&& dpkg-reconfigure --frontend noninteractive locales \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install zip exif iconv mbstring pcntl sockets xsl intl pdo_mysql gettext bcmath mcrypt \
&& pecl install redis amqp-1.9.3 zmq-beta imagick-beta \
&& pecl install \
redis \
amqp-1.9.3 \
zmq-beta \
imagick-beta \
xdebug-2.6.1 \
&& docker-php-ext-enable redis amqp zmq imagick \
&& pecl clear-cache \
&& docker-php-source delete \
&& rm -rf /var/lib/apt/lists/*
&& mkdir /tmp/ffmpeg \
&& curl -s https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.bz2 | tar jxf - -C /tmp/ffmpeg \
&& ( \
cd /tmp/ffmpeg/ffmpeg-${FFMPEG_VERSION} \
&& ./configure \
--enable-gpl \
--enable-nonfree \
--enable-libfdk-aac \
--enable-libfdk_aac \
--enable-libgsm \
--enable-libmp3lame \
--enable-libtheora \
--enable-libvorbis \
--enable-libvpx \
--enable-libfreetype \
--enable-libopus \
--enable-libx264 \
--enable-libxvid \
--enable-zlib \
--enable-postproc \
--enable-swscale \
--enable-pthreads \
--enable-libdc1394 \
--enable-version3 \
--enable-libopencore-amrnb \
--enable-libopencore-amrwb \
&& make \
&& make install \
&& make distclean \
) \
&& rm -rf /tmp/ffmpeg \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists \
&& mkdir /entrypoint /var/alchemy \
&& useradd -u 1000 app \
&& mkdir -p /home/app/.composer \
&& chown -R app: /home/app /var/alchemy
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
&& php composer-setup.php --install-dir=/usr/local/bin --filename=composer \
&& php -r "unlink('composer-setup.php');"
ENV XDEBUG_ENABLED=0
#########################################################################
# This image is used to build the apps
#########################################################################
FROM phraseanet-system as builder
COPY --from=composer:1.9.1 /usr/bin/composer /usr/bin/composer
# Node Installation (node + yarn)
# Reference :
# https://linuxize.com/post/how-to-install-node-js-on-ubuntu-18.04/
# https://yarnpkg.com/lang/en/docs/install/#debian-stable
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - \
&& apt install -y nodejs \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update && apt-get install -y --no-install-recommends yarn \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/
RUN mkdir /entrypoint /var/alchemy \
&& useradd -u 1000 app \
&& mkdir -p /home/app/.composer \
&& chown -R app: /home/app /var/alchemy
WORKDIR /var/alchemy/
COPY gulpfile.js /var/alchemy/
COPY Makefile /var/alchemy/
COPY package.json /var/alchemy/
COPY phpunit.xml.dist /var/alchemy/
COPY yarn.lock /var/alchemy/
COPY bin /var/alchemy/bin
COPY composer.json /var/alchemy/
COPY composer.lock /var/alchemy/
RUN make install_composer
COPY resources /var/alchemy/resources
COPY www /var/alchemy/www
RUN make clean_assets
RUN make install_asset_dependencies
RUN make install_assets
ADD ./docker/phraseanet/ /
COPY lib /var/alchemy/lib
COPY tmp /var/alchemy/tmp
COPY config /var/alchemy/config
COPY grammar /var/alchemy/grammar
COPY templates-profiler /var/alchemy/templates-profiler
COPY templates /var/alchemy/templates
COPY tests /var/alchemy/tests
# Phraseanet
FROM php:7.0-fpm-stretch as phraseanet-fpm
RUN apt-get update \
&& apt-get install -y \
apt-transport-https \
ca-certificates \
gnupg2 \
&& apt-get update \
&& apt-get install -y --no-install-recommends zlib1g-dev \
gettext \
git \
ghostscript \
gpac \
imagemagick \
libav-tools \
libfreetype6-dev \
libicu-dev \
libjpeg62-turbo-dev \
libmagickwand-dev \
libmcrypt-dev \
libpng-dev \
librabbitmq-dev \
libssl-dev \
libxslt-dev \
libzmq3-dev \
locales \
mcrypt \
swftools \
unoconv \
unzip \
xpdf \
&& update-locale "LANG=fr_FR.UTF-8 UTF-8" \
&& dpkg-reconfigure --frontend noninteractive locales \
&& docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install zip exif iconv mbstring pcntl sockets xsl intl pdo_mysql gettext bcmath mcrypt \
&& pecl install redis amqp-1.9.3 zmq-beta imagick-beta \
&& docker-php-ext-enable redis amqp zmq imagick \
&& pecl clear-cache \
&& docker-php-source delete \
&& rm -rf /var/lib/apt/lists/*
&& apt-get install -y --no-install-recommends \
nodejs \
yarn \
nano \
vim \
iputils-ping \
zsh \
ssh \
telnet \
autoconf \
libtool \
python \
pkg-config \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists \
&& git clone https://github.com/robbyrussell/oh-my-zsh.git /bootstrap/.oh-my-zsh \
&& mkdir -p /var/alchemy/Phraseanet \
&& chown -R app:app /var/alchemy
RUN mkdir /entrypoint /var/alchemy \
&& useradd -u 1000 app \
&& mkdir -p /home/app/.composer \
&& chown -R app: /home/app /var/alchemy
COPY --from=builder --chown=app /var/alchemy /var/alchemy/Phraseanet
ADD ./docker/phraseanet/ /
RUN mkdir -p /var/alchemy/Phraseanet/logs \
&& chmod -R 777 /var/alchemy/Phraseanet/logs \
&& mkdir -p /var/alchemy/Phraseanet/cache \
&& chmod -R 777 /var/alchemy/Phraseanet/cache \
&& mkdir -p /var/alchemy/Phraseanet/datas \
&& chmod -R 777 /var/alchemy/Phraseanet/datas \
&& mkdir -p /var/alchemy/Phraseanet/tmp \
&& chmod -R 777 /var/alchemy/Phraseanet/tmp \
&& mkdir -p /var/alchemy/Phraseanet/www/custom \
&& chmod -R 777 /var/alchemy/Phraseanet/www/custom \
&& mkdir -p /var/alchemy/Phraseanet/config \
&& chmod -R 777 /var/alchemy/Phraseanet/config
WORKDIR /var/alchemy/Phraseanet
ENTRYPOINT ["/phraseanet-entrypoint.sh"]
CMD ["/boot.sh"]
# phraseanet-worker
USER app
# Warm up composer cache for faster builds
COPY docker/caching/composer.* ./
RUN composer install --prefer-dist --no-dev --no-progress --no-suggest --classmap-authoritative --no-interaction --no-scripts \
&& rm -rf vendor composer.*
# End warm up
COPY --chown=app . .
RUN make install
ADD ./docker/builder/root /
# SSH Private repo
ARG SSH_PRIVATE_KEY
ARG PHRASEANET_PLUGINS
RUN ( \
test ! -z "${SSH_PRIVATE_KEY}" \
&& mkdir -p ~/.ssh \
&& echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa \
# make sure github domain.com is accepted
&& ssh-keyscan -H github.com >> ~/.ssh/known_hosts \
&& chmod 600 ~/.ssh/id_rsa \
) || echo "Skip SSH key"
RUN ./docker/phraseanet/plugins/console install
ENTRYPOINT ["/bootstrap/entrypoint.sh"]
CMD []
#########################################################################
# Phraseanet web application image
#########################################################################
FROM phraseanet-system as phraseanet-fpm
COPY --from=builder --chown=app /var/alchemy/Phraseanet /var/alchemy/Phraseanet
ADD ./docker/phraseanet/root /
WORKDIR /var/alchemy/Phraseanet
ENTRYPOINT ["docker/phraseanet/entrypoint.sh"]
CMD ["php-fpm", "-F"]
#########################################################################
# Phraseanet worker application image
#########################################################################
FROM phraseanet-fpm as phraseanet-worker
CMD ["/worker-boot.sh"]
ENTRYPOINT ["docker/phraseanet/worker/entrypoint.sh"]
CMD ["bin/console", "task-manager:scheduler:run"]
#########################################################################
# phraseanet-nginx
FROM nginx:1.15 as phraseanet-nginx
RUN useradd -u 1000 app
ADD ./docker/nginx/ /
COPY --from=builder /var/alchemy/www /var/alchemy/Phraseanet/www
CMD ["/boot.sh"]
#########################################################################
FROM nginx:1.17.8-alpine as phraseanet-nginx
RUN adduser --uid 1000 --disabled-password app
ADD ./docker/nginx/root /
COPY --from=builder /var/alchemy/Phraseanet/www /var/alchemy/Phraseanet/www
ENTRYPOINT ["/entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -1,18 +0,0 @@
ARG phraseanet
FROM $phraseanet
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
iproute2 \
&& rm -rf /var/lib/apt/lists/* \
&& pecl install xdebug \
&& docker-php-ext-enable xdebug \
&& pecl clear-cache
ADD ./docker/phraseanet-debug/ /
RUN chmod +x /entrypoint.sh /usr/local/bin/docker-*
ENTRYPOINT ["/entrypoint.sh"]
CMD ["php-fpm"]

View File

@@ -5,7 +5,10 @@ install:
make install_assets
install_composer:
composer install --ignore-platform-reqs
composer install --ignore-platform-reqs --no-dev --no-suggest --classmap-authoritative --no-interaction
install_composer_dev:
composer install
install_asset_dependencies:
yarn install

192
README.md
View File

@@ -18,6 +18,8 @@ Phraseanet is licensed under GPL-v3 license.
https://docs.phraseanet.com/
For development with Phraseanet API see https://docs.phraseanet.com/4.0/en/Devel/index.html
# Installation :
You **must** not download the source from GitHub, but download a packaged version here :
@@ -26,13 +28,176 @@ https://www.phraseanet.com/download/
And follow the install steps described at https://docs.phraseanet.com/4.0/en/Admin/Install.html
# Try Phraseanet :
# Phraseanet with Docker:
## Prerequisites
- docker-compose
- docker >=v18.01-ce
## Get started
You should review the default env variables defined in `.env` file.
Use `export` to override these values.
i.e:
```bash
export PHRASEANET_DOCKER_TAG=latest
export INSTALL_ACCOUNT_EMAIL=foo@bar.com
export INSTALL_ACCOUNT_PASSWORD=$3cr3t!
export PHRASEANET_APP_PORT=8082
```
### Using a env.local (custom .env)
It may be easier to deal with a local file to manage our env variables.
You can add your `env.local` at the root of this project and define a command function in your `~/.bashrc`:
```bash
# ~/.bashrc or ~/.zshrc
function dc() {
if [ -f env.local ]; then
env $(cat env.local | grep -v '#' | tr '\n' ' ') docker-compose $@
else
docker-compose $@
fi
}
```
### Running the application
If you are not interested in the development of Phraseanet, you can ignore everything in `.env` after the `DEV Purpose` part.
docker-compose -f docker-compose.yml up -d
Why this option `-f docker-compose.yml`?
The development and integration concerns are separated using a `docker-compose.override.yml`. By default, `docker-compose` will include this files if it exists.
If you don't work on phraseanet development, avoiding this `-f docker-compose.yml` parameters will throw errors. So you have to add this options on every `docker-compose` commands to avoid this inclusion.
> You can also delete the `docker-compose.override.yml` to get free from this behavior.
#### Running workers
```bash
docker-compose -f docker-compose.yml run --rm worker <command>
```
Where `<command>` can be:
- `bin/console task-manager:scheduler:run` (default)
- `bin/console worker:execute -m 2`
- ...
The default parameters allow you to reach the app with : `http://localhost:8082`
### Use Phraseanet images from docker hub
Retrieve on Docker hub prebuilt images for Phraseanet.
https://hub.docker.com/r/alchemyfr/phraseanet-fpm
https://hub.docker.com/r/alchemyfr/phraseanet-worker
https://hub.docker.com/r/alchemyfr/phraseanet-nginx
To use them and not build the images locally, we advise to override the properties in file: env.local
```bash
# Registry from where you pull Docker images
PHRASEANET_DOCKER_REGISTRY=alchemyfr
# Tag of the Docker images
PHRASEANET_DOCKER_TAG=
```
## Development mode
The development mode uses the `docker-compose-override.yml` file.
You can run it with:
docker-compose up -d
The environment is not ready yet: you have to fetch all dependencies.
This can be made easily from the builder container:
docker-compose run --rm -u app builder make install install_composer_dev
> Please note that the phraseanet image does not contain nor `composer` neither `node` tools. This allow the final image to be slim.
> If you need to use dev tools, ensure you are running the `builder` image!
### Developer shell
You can also obtain a shell access in builder container:
```bash
docker-compose run --rm builder /bin/bash
# or
docker-compose run --rm builder /bin/zsh
```
In this container you will have the same libraries (PHP, Node, composer, ...) that are used to build images.
Also you have utils for development like telnet, ping, ssh, git, ...
Your $HOME/.ssh directory is also mounted to builder's home with your ssh agent.
### Using Xdebug
Xdebug is enabled by default with the `docker-compose.override.yml`
You can disable it by setting:
```bash
export XDEBUG_ENABLED=0
```
Remote host is fixed because of the subnet network from compose.
You need to configure file mapping in your IDE.
For PhpStorm, you can follow this example:
![PhpStorm mapping](https://i.ibb.co/GMb43Cv/image.png)
> Configure the `Absolute path on the server` to `/var/alchemy/Phraseanet` at the project root path (i.e. `~/projects/Phraseanet`).
#### Xdebug on MacOS
You have to set the following env:
```bash
XDEBUG_REMOTE_HOST=host.docker.internal
```
> Don't forget to recreate your container (`docker-compose up -d phraseanet`)
### Build images with plugins
Plugins can be installed during build if you set the `PHRASEANET_PLUGINS` env var as follows:
```bash
PHRASEANET_PLUGINS="git@github.com:alchemy-fr/Phraseanet-plugin-webgallery.git"
# You can optionally precise the branch to install
# If not precised, the main branch will be pulled
PHRASEANET_PLUGINS="git@github.com:alchemy-fr/Phraseanet-plugin-webgallery.git(custom-branch)"
# Plugins are separated by spaces
PHRASEANET_PLUGINS="git@github.com:foo/bar.git(branch-1) git@github.com:baz/42.git"
```
If you install private plugins, make sure you export your SSH private key content in order to allow docker build to access the GIT repository:
```bash
export PHRASEANET_SSH_PRIVATE_KEY=$(cat ~/.ssh/id_rsa)
# or if your private key is protected by a passphrase:
export PHRASEANET_SSH_PRIVATE_KEY=$(openssl rsa -in ~/.ssh/id_rsa -out /tmp/id_rsa_raw && cat /tmp/id_rsa_raw && rm /tmp/id_rsa_raw)
```
# Try Phraseanet with Pre installed VM (deprecated)
You can also download a testing pre installed Virtual Machine in OVA format here :
https://www.phraseanet.com/download/
# Development :
# With Vagrant (deprecated)
## Development :
For development purpose Phraseanet is shipped with ready to use development environments using vagrant.
You can easily choose betweeen a complete build or a prebuild box, with a specific PHP version.
@@ -49,28 +214,5 @@ Ex:
- vagrant up --provision //// 5.6 ///// 1 >> Build the alchemy/phraseanet-php-5.6 box
For development with Phraseanet API see https://docs.phraseanet.com/4.0/en/Devel/index.html
# Docker build
WARNING : still in a work-in-progress status and can be used only for test purposes.
The docker distribution come with 3 differents containers :
* An nginx that act as the front http server.
* The php-fpm who serves the php files through nginx.
* The worker who execute Phraseanet scheduler.
## How to build
The three images can be built respectively with these commands :
# nginx server
docker build --target phraseanet-nginx -t local/phraseanet-nginx .
# php-fpm application
docker build --target phraseanet-fpm -t local/phraseanet-fpm .
# worker
docker build --target phraseanet-worker -t local/phraseanet-worker .

View File

@@ -23,12 +23,19 @@ use Alchemy\Phrasea\Command\SearchEngine\IndexPopulateCommand;
use Alchemy\Phrasea\Command\Thesaurus\FindConceptsCommand;
use Alchemy\Phrasea\Core\Version;
use Alchemy\Phrasea\Command\CreateCollection;
use Alchemy\Phrasea\Command\Collection\UnPublishCollectionCommand;
use Alchemy\Phrasea\Command\Collection\PublishCollectionCommand;
use Alchemy\Phrasea\Command\Collection\ListCollectionCommand;
use Alchemy\Phrasea\Command\Databox\CreateDataboxCommand;
use Alchemy\Phrasea\Command\Databox\UnMountDataboxCommand;
use Alchemy\Phrasea\Command\Databox\MountDataboxCommand;
use Alchemy\Phrasea\Command\Databox\ListDataboxCommand;
use Alchemy\Phrasea\Command\MailTest;
use Alchemy\Phrasea\Command\Compile\Configuration;
use Alchemy\Phrasea\Command\RecordAdd;
use Alchemy\Phrasea\Command\RescanTechnicalDatas;
use Alchemy\Phrasea\CLI;
use Alchemy\Phrasea\Command\User\UserApplicationsCommand;
use Alchemy\Phrasea\Command\Plugin\AddPlugin;
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
use Alchemy\Phrasea\Command\CheckConfig;
@@ -46,7 +53,11 @@ use Alchemy\Phrasea\Command\Task\TaskRun;
use Alchemy\Phrasea\Command\Task\TaskStart;
use Alchemy\Phrasea\Command\Task\TaskState;
use Alchemy\Phrasea\Command\Task\TaskStop;
use Alchemy\Phrasea\Command\User\UserCreateCommand;
use Alchemy\Phrasea\Command\User\UserPasswordCommand;
use Alchemy\Phrasea\Command\User\UserListCommand;
use Alchemy\Phrasea\Command\UpgradeDBDatas;
use Alchemy\Phrasea\Command\ApplyRightsCommand;
require_once __DIR__ . '/../lib/autoload.php';
@@ -83,6 +94,7 @@ $cli->command(new \module_console_aboutLicense('about:license'));
$cli->command(new CheckConfig('check:config'));
$cli->command(new UpgradeDBDatas('system:upgrade-datas'));
$cli->command(new ApplyRightsCommand('system:apply-rights'));
$cli->command(new \module_console_systemMailCheck('system:mail-check'));
$cli->command(new \module_console_systemBackupDB('system:backup-db'));
@@ -107,8 +119,25 @@ $cli->command(new \module_console_fieldsDelete('fields:delete'));
$cli->command(new \module_console_fieldsRename('fields:rename'));
$cli->command(new \module_console_fieldsMerge('fields:merge'));
$cli->command(new UserApplicationsCommand('user:applications'));
$cli->command(new CreateCollection('collection:create'));
$cli->command(new UnPublishCollectionCommand('collection:unpublish'));
$cli->command(new PublishCollectionCommand('collection:publish'));
$cli->command(new ListCollectionCommand('collection:list'));
$cli->command(new ListDataboxCommand('databox:list'));
$cli->command(new CreateDataboxCommand('databox:create'));
$cli->command(new UnMountDataboxCommand('databox:unmount'));
$cli->command(new MountDataboxCommand('databox:mount'));
$cli->command(new UserCreateCommand('user:create'));
$cli->command(new UserPasswordCommand('user:password'));
$cli->command(new UserListCommand('user:list'));
$cli->command(new RecordAdd('records:add'));
$cli->command(new RescanTechnicalDatas('records:rescan-technical-datas'));
@@ -133,9 +162,9 @@ $cli->command(new QueryParseCommand());
$cli->command(new QuerySampleCommand());
$cli->command(new FindConceptsCommand());
$cli->command($cli['alchemy_worker.commands.run_dispatcher_command']);
$cli->command($cli['alchemy_worker.commands.run_worker_command']);
$cli->command($cli['alchemy_worker.commands.show_configuration']);
//$cli->command($cli['alchemy_worker.commands.run_dispatcher_command']);
//$cli->command($cli['alchemy_worker.commands.run_worker_command']);
//$cli->command($cli['alchemy_worker.commands.show_configuration']);
$cli->loadPlugins();

View File

@@ -18,6 +18,18 @@
}
}
},
{
"type": "package",
"package": {
"name": "exiftool/exiftool",
"version": "11",
"source": {
"url": "https://github.com/exiftool/exiftool.git",
"type": "git",
"reference": "11.84"
}
}
},
{
"type": "vcs",
"url": "https://github.com/alchemy-fr/tcpdf-clone"
@@ -84,7 +96,7 @@
"league/flysystem": "^1.0",
"league/flysystem-aws-s3-v2": "^1.0",
"league/fractal": "dev-webgalleries#af1acc0275438571bc8c1d08a05a4b5af92c9f97 as 0.13.0",
"media-alchemyst/media-alchemyst": "^0.5",
"media-alchemyst/media-alchemyst": "^0.5.6",
"monolog/monolog": "~1.3",
"mrclay/minify": "~2.1.6",
"neutron/process-manager": "2.0.x-dev@dev",
@@ -93,9 +105,9 @@
"neutron/silex-imagine-provider": "~0.1.0",
"neutron/temporary-filesystem": "~2.1",
"pagerfanta/pagerfanta": "^1.0",
"php-ffmpeg/php-ffmpeg": "~0.5.0",
"php-ffmpeg/php-ffmpeg": "^v0.15",
"php-xpdf/php-xpdf": "~0.2.1",
"phpexiftool/exiftool": "10.10",
"exiftool/exiftool": "^11",
"ramsey/uuid": "^3.0",
"roave/security-advisories": "dev-master",
"silex/silex": "^1.3.0",

267
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f3b1fc0a30bf14b05e57ce673550d9c0",
"content-hash": "008ff0b5d3d13b4f0ce5d34348ded83a",
"packages": [
{
"name": "alchemy-fr/tcpdf-clone",
@@ -131,16 +131,16 @@
},
{
"name": "alchemy/embed-bundle",
"version": "2.0.7",
"version": "2.0.9",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/embed-bundle.git",
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde"
"reference": "8cdb9612a9e3edd998b68f0803eacca8e0f50775"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/8cdb9612a9e3edd998b68f0803eacca8e0f50775",
"reference": "8cdb9612a9e3edd998b68f0803eacca8e0f50775",
"shasum": ""
},
"require-dev": {
@@ -178,10 +178,10 @@
],
"description": "Embed resources bundle",
"support": {
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.7",
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.9",
"issues": "https://github.com/alchemy-fr/embed-bundle/issues"
},
"time": "2019-09-02T12:28:19+00:00"
"time": "2020-02-04T14:53:00+00:00"
},
{
"name": "alchemy/geonames-api-consumer",
@@ -275,16 +275,16 @@
},
{
"name": "alchemy/mediavorus",
"version": "0.4.9",
"version": "0.4.10",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/MediaVorus.git",
"reference": "1a96dc4142ff8474c11285cab9eab11df9683255"
"reference": "3e235eb1efb528aea2973c946f4bf47630b98985"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/MediaVorus/zipball/1a96dc4142ff8474c11285cab9eab11df9683255",
"reference": "1a96dc4142ff8474c11285cab9eab11df9683255",
"url": "https://api.github.com/repos/alchemy-fr/MediaVorus/zipball/3e235eb1efb528aea2973c946f4bf47630b98985",
"reference": "3e235eb1efb528aea2973c946f4bf47630b98985",
"shasum": ""
},
"require": {
@@ -300,6 +300,7 @@
},
"require-dev": {
"jms/serializer": "~0.12.0",
"phpunit/phpunit": "^4.0|^5.0",
"silex/silex": "~1.0",
"symfony/yaml": "~2.0"
},
@@ -333,7 +334,7 @@
"keywords": [
"metadata"
],
"time": "2019-01-22T11:23:34+00:00"
"time": "2020-02-18T13:37:45+00:00"
},
{
"name": "alchemy/oauth2php",
@@ -383,30 +384,27 @@
},
{
"name": "alchemy/phpexiftool",
"version": "0.7.2",
"version": "0.7.3",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/PHPExiftool.git",
"reference": "ba1cb51eceb6562d7996023478977a8739de188b"
"reference": "0b22e7d7cc40f2a6b9c85c0cfbd968a39a31dab2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/ba1cb51eceb6562d7996023478977a8739de188b",
"reference": "ba1cb51eceb6562d7996023478977a8739de188b",
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/0b22e7d7cc40f2a6b9c85c0cfbd968a39a31dab2",
"reference": "0b22e7d7cc40f2a6b9c85c0cfbd968a39a31dab2",
"shasum": ""
},
"require": {
"doctrine/cache": "^1.0",
"doctrine/collections": "^1.0",
"exiftool/exiftool": "^11",
"monolog/monolog": "^1.3",
"php": ">=5.5.9",
"phpexiftool/exiftool": "10.10",
"symfony/console": "^2.1|^3.0",
"symfony/process": "^2.1|^3.0"
},
"replace": {
"phpexiftool/phpexiftool": "<0.5.0"
},
"require-dev": {
"jms/serializer": "~0.10|^1.0",
"phpunit/phpunit": "^4.0|^5.0",
@@ -452,7 +450,7 @@
"exiftool",
"metadata"
],
"time": "2019-02-13T13:06:43+00:00"
"time": "2020-01-17T14:28:33+00:00"
},
{
"name": "alchemy/queue-bundle",
@@ -2105,6 +2103,16 @@
],
"time": "2012-05-30T15:01:08+00:00"
},
{
"name": "exiftool/exiftool",
"version": "11",
"source": {
"type": "git",
"url": "https://github.com/exiftool/exiftool.git",
"reference": "11.84"
},
"type": "library"
},
{
"name": "facebook/graph-sdk",
"version": "5.6.1",
@@ -2853,6 +2861,7 @@
}
],
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"abandoned": true,
"time": "2015-05-20T03:37:09+00:00"
},
{
@@ -2903,6 +2912,7 @@
"Guzzle",
"stream"
],
"abandoned": true,
"time": "2014-10-12T19:18:40+00:00"
},
{
@@ -4321,16 +4331,16 @@
},
{
"name": "media-alchemyst/media-alchemyst",
"version": "0.5.2",
"version": "0.5.6",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/Media-Alchemyst.git",
"reference": "5d2fe6dd95215804202ecf0466fd9cfaeedd0140"
"reference": "2b9f7697997f7863bbc3d08344c559a1cba519c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/Media-Alchemyst/zipball/5d2fe6dd95215804202ecf0466fd9cfaeedd0140",
"reference": "5d2fe6dd95215804202ecf0466fd9cfaeedd0140",
"url": "https://api.github.com/repos/alchemy-fr/Media-Alchemyst/zipball/2b9f7697997f7863bbc3d08344c559a1cba519c2",
"reference": "2b9f7697997f7863bbc3d08344c559a1cba519c2",
"shasum": ""
},
"require": {
@@ -4340,9 +4350,9 @@
"monolog/monolog": "~1.0",
"neutron/temporary-filesystem": "^2.1.1",
"php": ">=5.3.3",
"php-ffmpeg/php-ffmpeg": ">=0.4.2,<0.6",
"php-ffmpeg/php-ffmpeg": "^v0.15",
"php-mp4box/php-mp4box": "~0.3.0",
"php-unoconv/php-unoconv": "~0.3.0",
"php-unoconv/php-unoconv": "~0.3.1",
"pimple/pimple": "~1.0",
"swftools/swftools": "~0.3.0",
"symfony/console": "^2.1|^3.0",
@@ -4391,7 +4401,7 @@
"video",
"video processing"
],
"time": "2019-01-25T12:09:11+00:00"
"time": "2020-04-01T08:51:55+00:00"
},
{
"name": "monolog/monolog",
@@ -5137,29 +5147,29 @@
},
{
"name": "php-ffmpeg/php-ffmpeg",
"version": "0.5.1",
"version": "v0.15",
"source": {
"type": "git",
"url": "https://github.com/PHP-FFMpeg/PHP-FFMpeg.git",
"reference": "c8949fe3df89edd7692368cc110a51a27971f28a"
"reference": "984dbd046b6d8c285f9e7419fc7645f197513bfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/c8949fe3df89edd7692368cc110a51a27971f28a",
"reference": "c8949fe3df89edd7692368cc110a51a27971f28a",
"url": "https://api.github.com/repos/PHP-FFMpeg/PHP-FFMpeg/zipball/984dbd046b6d8c285f9e7419fc7645f197513bfa",
"reference": "984dbd046b6d8c285f9e7419fc7645f197513bfa",
"shasum": ""
},
"require": {
"alchemy/binary-driver": "~1.5",
"doctrine/cache": "~1.0",
"evenement/evenement": "~1.0",
"neutron/temporary-filesystem": "~2.1, >=2.1.1",
"php": ">=5.3.3"
"alchemy/binary-driver": "^1.5 || ~2.0.0 || ^5.0",
"doctrine/cache": "^1.0",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"neutron/temporary-filesystem": "^2.1.1",
"php": "^5.3.9 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "~3.7",
"sami/sami": "~1.0",
"silex/silex": "~1.0"
"silex/silex": "~1.0",
"symfony/phpunit-bridge": "^5.0.4"
},
"suggest": {
"php-ffmpeg/extras": "A compilation of common audio & video drivers for PHP-FFMpeg"
@@ -5167,7 +5177,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "0.5-dev"
"dev-master": "0.7-dev"
}
},
"autoload": {
@@ -5189,6 +5199,21 @@
"name": "Phraseanet Team",
"email": "info@alchemy.fr",
"homepage": "http://www.phraseanet.com/"
},
{
"name": "Patrik Karisch",
"email": "patrik@karisch.guru",
"homepage": "http://www.karisch.guru"
},
{
"name": "Romain Biard",
"email": "romain.biard@gmail.com",
"homepage": "https://www.strime.io/"
},
{
"name": "Jens Hausdorf",
"email": "hello@jens-hausdorf.de",
"homepage": "https://jens-hausdorf.de"
}
],
"description": "FFMpeg PHP, an Object Oriented library to communicate with AVconv / ffmpeg",
@@ -5202,7 +5227,7 @@
"video",
"video processing"
],
"time": "2014-08-26T08:46:56+00:00"
"time": "2020-03-23T09:32:09+00:00"
},
{
"name": "php-mp4box/php-mp4box",
@@ -5257,16 +5282,16 @@
},
{
"name": "php-unoconv/php-unoconv",
"version": "0.3.0",
"version": "0.3.1",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/PHP-Unoconv.git",
"reference": "6d1e14a7467b5d637741396549529dc4d5f9f355"
"reference": "8fa666972f6c13fe9703dfe894cd311a61f89f33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/PHP-Unoconv/zipball/6d1e14a7467b5d637741396549529dc4d5f9f355",
"reference": "6d1e14a7467b5d637741396549529dc4d5f9f355",
"url": "https://api.github.com/repos/alchemy-fr/PHP-Unoconv/zipball/8fa666972f6c13fe9703dfe894cd311a61f89f33",
"reference": "8fa666972f6c13fe9703dfe894cd311a61f89f33",
"shasum": ""
},
"require": {
@@ -5303,7 +5328,7 @@
"keywords": [
"unoconv"
],
"time": "2013-06-25T10:09:59+00:00"
"time": "2019-09-16T09:54:30+00:00"
},
{
"name": "php-xpdf/php-xpdf",
@@ -5405,39 +5430,6 @@
],
"time": "2015-05-17T12:39:23+00:00"
},
{
"name": "phpexiftool/exiftool",
"version": "10.10",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/exiftool.git",
"reference": "0833cab894c890353192a83011428525a318bedf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/exiftool/zipball/0833cab894c890353192a83011428525a318bedf",
"reference": "0833cab894c890353192a83011428525a318bedf",
"shasum": ""
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"Perl Licensing"
],
"authors": [
{
"name": "Phil Harvey",
"email": "phil@owl.phy.queensu.ca",
"homepage": "http://www.sno.phy.queensu.ca/~phil/exiftool/"
}
],
"description": "Exiftool is a library for reading, writing and editing meta information. This package is not PHP, but required for the main PHP driver : PHP Exiftool",
"keywords": [
"exiftool",
"metadatas"
],
"time": "2016-01-25T11:10:14+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.5.0",
@@ -5902,6 +5894,12 @@
"url": "https://github.com/Roave/SecurityAdvisories.git",
"reference": "0698207bf8a9bed212fdde2d8c7cdc77085660c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/0698207bf8a9bed212fdde2d8c7cdc77085660c4",
"reference": "0698207bf8a9bed212fdde2d8c7cdc77085660c4",
"shasum": ""
},
"conflict": {
"adodb/adodb-php": "<5.20.6",
"amphp/artax": ">=2,<2.0.4|>0.7.1,<1.0.4",
@@ -6186,7 +6184,7 @@
],
"description": "A WebProfiler for Silex",
"homepage": "http://silex.sensiolabs.org/",
"abandoned": true,
"abandoned": "symfony/web-profiler-bundle",
"time": "2016-01-10T11:39:13+00:00"
},
{
@@ -6436,6 +6434,7 @@
"profiler",
"silex"
],
"abandoned": true,
"time": "2016-10-26T11:08:02+00:00"
},
{
@@ -6478,6 +6477,7 @@
"plugin",
"silex"
],
"abandoned": true,
"time": "2015-11-11T07:16:28+00:00"
},
{
@@ -7726,6 +7726,7 @@
"code",
"zf2"
],
"abandoned": "laminas/laminas-code",
"time": "2016-04-20T17:26:42+00:00"
},
{
@@ -7780,12 +7781,13 @@
"events",
"zf2"
],
"abandoned": "laminas/laminas-eventmanager",
"time": "2016-02-18T20:53:00+00:00"
}
],
"packages-dev": [
{
"name": "mikey179/vfsStream",
"name": "mikey179/vfsstream",
"version": "v1.6.4",
"source": {
"type": "git",
@@ -7830,76 +7832,6 @@
"homepage": "http://vfs.bovigo.org/",
"time": "2016-07-18T14:02:57+00:00"
},
{
"name": "php-amqplib/php-amqplib",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/php-amqplib/php-amqplib.git",
"reference": "fa2f0d4410a11008cb36b379177291be7ee9e4f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-amqplib/php-amqplib/zipball/fa2f0d4410a11008cb36b379177291be7ee9e4f6",
"reference": "fa2f0d4410a11008cb36b379177291be7ee9e4f6",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"ext-mbstring": "*",
"php": ">=5.3.0"
},
"replace": {
"videlalvaro/php-amqplib": "self.version"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"scrutinizer/ocular": "^1.1",
"squizlabs/php_codesniffer": "^2.5"
},
"suggest": {
"ext-sockets": "Use AMQPSocketConnection"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7-dev"
}
},
"autoload": {
"psr-4": {
"PhpAmqpLib\\": "PhpAmqpLib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "Alvaro Videla",
"role": "Original Maintainer"
},
{
"name": "John Kelly",
"email": "johnmkelly86@gmail.com",
"role": "Maintainer"
},
{
"name": "Raúl Araya",
"email": "nubeiro@gmail.com",
"role": "Maintainer"
}
],
"description": "Formerly videlalvaro/php-amqplib. This library is a pure PHP implementation of the AMQP protocol. It's been tested against RabbitMQ.",
"homepage": "https://github.com/php-amqplib/php-amqplib/",
"keywords": [
"message",
"queue",
"rabbitmq"
],
"time": "2016-04-11T14:30:01+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "1.0",
@@ -8046,6 +7978,39 @@
],
"time": "2016-11-25T06:54:22+00:00"
},
{
"name": "phpexiftool/exiftool",
"version": "10.10",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/exiftool.git",
"reference": "0833cab894c890353192a83011428525a318bedf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/exiftool/zipball/0833cab894c890353192a83011428525a318bedf",
"reference": "0833cab894c890353192a83011428525a318bedf",
"shasum": ""
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"Perl Licensing"
],
"authors": [
{
"name": "Phil Harvey",
"email": "phil@owl.phy.queensu.ca",
"homepage": "http://www.sno.phy.queensu.ca/~phil/exiftool/"
}
],
"description": "Exiftool is a library for reading, writing and editing meta information. This package is not PHP, but required for the main PHP driver : PHP Exiftool",
"keywords": [
"exiftool",
"metadatas"
],
"time": "2016-01-25T11:10:14+00:00"
},
{
"name": "phpspec/prophecy",
"version": "v1.6.2",

View File

@@ -22,13 +22,54 @@ main:
path: '/tmp/db.sqlite'
charset: UTF8
cache:
type: MemcacheCache
type: redis
options:
host: localhost
port: 11211
port: 6379
search-engine:
type: phrasea
options: []
type: elasticsearch
options:
host: elasticsearch
port: 9200
index: ''
shards: 3
replicas: 0
minScore: 2
highlight: true
populate_order: RECORD_ID
populate_direction: DESC
activeTab: ''
facets:
_base:
limit: 10
_collection:
limit: 10
_doctype:
limit: 10
_camera_model:
limit: 0
_iso:
limit: 0
_aperture:
limit: 0
_shutterspeed:
limit: 0
_flashfired:
limit: 0
_framerate:
limit: 0
_audiosamplerate:
limit: 0
_videocodec:
limit: 0
_audiocodec:
limit: 0
_orientation:
limit: 0
_colorspace:
limit: 0
_mimetype:
limit: 0
task-manager:
status: started
enabled: true
@@ -62,12 +103,7 @@ main:
mp4box_timeout: 60
swftools_timeout: 60
unoconv_timeout: 60
task-manager:
status: started
listener:
protocol: tcp
host: 127.0.0.1
port: 6700
exiftool_timeout: 60
storage:
subdefs: null
cache: null
@@ -75,20 +111,7 @@ main:
download: null
lazaret: null
caption: null
bridge:
youtube:
enabled: false
client_id: null
client_secret: null
developer_key: null
flickr:
enabled: false
client_id: null
client_secret: null
dailymotion:
enabled: false
client_id: null
client_secret: null
worker_tmp_files: null
border-manager:
enabled: true
extension-mapping:
@@ -98,12 +121,17 @@ border-manager:
-
type: Checker\Sha256
enabled: true
collections: []
compare-ignore-collections: []
-
type: Checker\UUID
enabled: true
collections: []
compare-ignore-collections: []
-
type: Checker\Colorspace
enabled: false
collections: []
options:
colorspaces: [cmyk, grayscale, rgb]
media_types: [Image]
@@ -123,6 +151,8 @@ border-manager:
enabled: false
options:
sensitive: true
collections: []
compare-ignore-collections: []
-
type: Checker\MediaType
enabled: false
@@ -244,6 +274,14 @@ embed_bundle:
document:
player: flexpaper
enable_pdfjs: true
video-editor:
ChapterVttFieldName: VideoTextTrackChapters
seekBackwardStep: 500 # in ms
seekForwardStep: 500 # in ms
playbackRates:
- 1
- '1.5'
- 3
geocoding-providers:
-
map-provider: mapboxWebGL

103
docker-compose.override.yml Normal file
View File

@@ -0,0 +1,103 @@
version: "3.4"
services:
phpmyadmin:
image: phpmyadmin/phpmyadmin
restart: on-failure
ports:
- ${PHRASEANET_PHPMYADMIN_PORT}:80
depends_on:
- db
gateway:
volumes:
- ../:/var/alchemy
- .:/var/alchemy/Phraseanet
- ./docker/nginx/root/entrypoint.sh:/entrypoint.sh
- ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw
- ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw
- ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw
- ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw
builder:
build:
context: .
target: builder
args:
- SSH_PRIVATE_KEY=${PHRASEANET_SSH_PRIVATE_KEY}
- PHRASEANET_PLUGINS=${PHRASEANET_PLUGINS}
stdin_open: true
tty: true
volumes:
- ../:/var/alchemy
- .:/var/alchemy/Phraseanet
- ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw
- ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw
- ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw
- ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw
- ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw
- ${SSH_AUTH_SOCK}:/ssh-auth-sock
- ${HOME}/.ssh:/home/app/.ssh
- dev_vol:/home/app
environment:
- PHRASEANET_PROJECT_NAME
phraseanet:
environment:
- XDEBUG_ENABLED
- XDEBUG_CONFIG=remote_host=${XDEBUG_REMOTE_HOST} idekey=${IDE_KEY} remote_enable=1 profiler_enable=${XDEBUG_PROFILER_ENABLED} profiler_output_dir=/var/alchemy/Phraseanet/cache/profiler
- PHP_IDE_CONFIG
volumes:
- ../:/var/alchemy
- .:/var/alchemy/Phraseanet
- ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw
- ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw
- ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw
- ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw
- ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw
- ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw
worker:
volumes:
- ../:/var/alchemy
- .:/var/alchemy/Phraseanet
- ${PHRASEANET_CONFIG_DIR}:/var/alchemy/Phraseanet/config:rw
- ${PHRASEANET_LOGS_DIR}:/var/alchemy/Phraseanet/logs:rw
- ${PHRASEANET_DATA_DIR}:/var/alchemy/Phraseanet/datas:rw
- ${PHRASEANET_THUMBNAILS_DIR}:/var/alchemy/Phraseanet/www/thumbnails:rw
- ${PHRASEANET_CUSTOM_DIR}:/var/alchemy/Phraseanet/www/custom:rw
- ${PHRASEANET_TMP_DIR}:/var/alchemy/Phraseanet/tmp:rw
rabbitmq:
ports:
- ${RABBITMQ_MANAGEMENT_PORT}:15672
db:
volumes:
- ${PHRASEANET_DB_DIR}:/var/lib/mysql:rw
mailhog:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
elasticsearch:
ports:
- 9200:9200
volumes:
- ${PHRASEANET_ELASTICSEARCH_DIR}:/usr/share/elasticsearch/data:rw
kibana:
image: kibana:4.6.6
ports:
- 5601:5601
networks:
default:
ipam:
config:
- subnet: $PHRASEANET_SUBNET_IPS
volumes:
dev_vol:
driver: local

143
docker-compose.yml Normal file
View File

@@ -0,0 +1,143 @@
version: "3.4"
services:
gateway:
build:
context: .
target: phraseanet-nginx
args:
- SSH_PRIVATE_KEY=${PHRASEANET_SSH_PRIVATE_KEY}
- PHRASEANET_PLUGINS=${PHRASEANET_PLUGINS}
image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-nginx:$PHRASEANET_DOCKER_TAG
restart: on-failure
volumes:
- data_vol:/var/alchemy/Phraseanet/datas:rw
- thumbnails_vol:/var/alchemy/Phraseanet/www/thumbnails:rw
- custom_vol:/var/alchemy/Phraseanet/www/custom:rw
depends_on:
- phraseanet
environment:
- MAX_BODY_SIZE
ports:
- ${PHRASEANET_APP_PORT}:80
phraseanet:
build:
context: .
target: phraseanet-fpm
args:
- SSH_PRIVATE_KEY=${PHRASEANET_SSH_PRIVATE_KEY}
- PHRASEANET_PLUGINS=${PHRASEANET_PLUGINS}
image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-fpm:$PHRASEANET_DOCKER_TAG
restart: on-failure
depends_on:
- db
- redis
- rabbitmq
- elasticsearch
environment:
- PHRASEANET_PROJECT_NAME
- MAX_BODY_SIZE
- MAX_INPUT_VARS
- OPCACHE_ENABLED
- SESSION_CACHE_LIMITER
- PHP_LOG_LEVEL
- INSTALL_ACCOUNT_EMAIL
- INSTALL_ACCOUNT_PASSWORD
- INSTALL_DB_HOST
- INSTALL_DB_PORT
- INSTALL_DB_USER
- INSTALL_DB_PASSWORD
- INSTALL_DB_TEMPLATE
- INSTALL_APPBOX
- INSTALL_DATABOX
- INSTALL_SERVER_NAME
- INSTALL_RABBITMQ_USER=$RABBITMQ_DEFAULT_USER
- INSTALL_RABBITMQ_PASSWORD=$RABBITMQ_DEFAULT_PASS
volumes:
- config_vol:/var/alchemy/Phraseanet/config:rw
- data_vol:/var/alchemy/Phraseanet/datas:rw
- tmp_vol:/var/alchemy/Phraseanet/tmp:rw
- logs_vol:/var/alchemy/Phraseanet/logs:rw
- thumbnails_vol:/var/alchemy/Phraseanet/www/thumbnails:rw
- custom_vol:/var/alchemy/Phraseanet/www/custom:rw
- cache_vol:/var/alchemy/Phraseanet/cache:rw
worker:
build:
context: .
target: phraseanet-worker
args:
- SSH_PRIVATE_KEY=${PHRASEANET_SSH_PRIVATE_KEY}
- PHRASEANET_PLUGINS=${PHRASEANET_PLUGINS}
image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-worker:$PHRASEANET_DOCKER_TAG
restart: on-failure
depends_on:
- db
- redis
- rabbitmq
- elasticsearch
environment:
- PHRASEANET_PROJECT_NAME
- MAX_BODY_SIZE
- MAX_INPUT_VARS
- OPCACHE_ENABLED
- SESSION_CACHE_LIMITER
- PHP_LOG_LEVEL
volumes:
- config_vol:/var/alchemy/Phraseanet/config:rw
- data_vol:/var/alchemy/Phraseanet/datas:rw
- tmp_vol:/var/alchemy/Phraseanet/tmp:rw
- logs_vol:/var/alchemy/Phraseanet/logs:rw
- thumbnails_vol:/var/alchemy/Phraseanet/www/thumbnails:rw
- custom_vol:/var/alchemy/Phraseanet/www/custom:rw
- cache_vol:/var/alchemy/Phraseanet/cache:rw
db:
image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-db:$PHRASEANET_DOCKER_TAG
build: ./docker/db
restart: on-failure
environment:
- MYSQL_ROOT_PASSWORD
- MYSQL_MAX_ALLOWED_PACKET
volumes:
- db_vol:/var/lib/mysql
rabbitmq:
image: rabbitmq:3-management
restart: on-failure
environment:
- RABBITMQ_DEFAULT_USER
- RABBITMQ_DEFAULT_PASS
redis:
image: redis
restart: on-failure
elasticsearch:
image: $PHRASEANET_DOCKER_REGISTRY/phraseanet-elasticsearch:$PHRASEANET_DOCKER_TAG
build: ./docker/elasticsearch
restart: on-failure
volumes:
- elasticsearch_vol:/usr/share/elasticsearch/data
volumes:
config_vol:
driver: local
data_vol:
driver: local
tmp_vol:
driver: local
db_vol:
driver: local
elasticsearch_vol:
driver: local
thumbnails_vol:
driver: local
custom_vol:
driver: local
cache_vol:
driver: local
# to be replacer by stdout/stderr
logs_vol:
driver: local

View File

@@ -0,0 +1,7 @@
local ret_status="%(?:%{$fg_bold[green]%}➜ :%{$fg_bold[red]%}➜ %s)"
PROMPT='%* %{$fg_bold[green]%}%n%{$fg[grey]%}@%m%{$fg_bold[green]%}%u ${ret_status}%{$fg_bold[green]%}%p %{$fg[cyan]%}%c %{$fg_bold[blue]%}$(git_prompt_info)%{$fg_bold[blue]%} % %{$reset_color%}'
ZSH_THEME_GIT_PROMPT_PREFIX="[%{$fg[red]%}"
ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%}"
ZSH_THEME_GIT_PROMPT_DIRTY="%{$fg[blue]%}] %{$fg[yellow]%}✗%{$reset_color%}"
ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg[blue]%}]"

View File

@@ -0,0 +1,56 @@
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export ZSH=$HOME/.oh-my-zsh
ZSH_THEME="alchemy"
# Uncomment the following line to use case-sensitive completion.
# CASE_SENSITIVE="true"
# Uncomment the following line to use hyphen-insensitive completion. Case
# sensitive completion must be off. _ and - will be interchangeable.
# HYPHEN_INSENSITIVE="true"
# Uncomment the following line to disable bi-weekly auto-update checks.
# DISABLE_AUTO_UPDATE="true"
# Uncomment the following line to change how often to auto-update (in days).
# export UPDATE_ZSH_DAYS=13
# Uncomment the following line to disable colors in ls.
# DISABLE_LS_COLORS="true"
# Uncomment the following line to disable auto-setting terminal title.
# DISABLE_AUTO_TITLE="true"
# Uncomment the following line to enable command auto-correction.
# ENABLE_CORRECTION="true"
# Uncomment the following line to display red dots whilst waiting for completion.
# COMPLETION_WAITING_DOTS="true"
# Uncomment the following line if you want to disable marking untracked files
# under VCS as dirty. This makes repository status check for large repositories
# much, much faster.
# DISABLE_UNTRACKED_FILES_DIRTY="true"
# Uncomment the following line if you want to change the command execution time
# stamp shown in the history command output.
# The optional three formats: "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd"
# HIST_STAMPS="mm/dd/yyyy"
# Would you like to use another custom folder than $ZSH/custom?
# ZSH_CUSTOM=/path/to/new-custom-folder
# Which plugins would you like to load? (plugins can be found in ~/.oh-my-zsh/plugins/*)
# Custom plugins may be added to ~/.oh-my-zsh/custom/plugins/
# Example format: plugins=(rails git textmate ruby lighthouse)
# Add wisely, as too many plugins slow down shell startup.
plugins=(git symfony2)
# User configuration
source $ZSH/oh-my-zsh.sh
alias ll='ls -alFh'

View File

@@ -0,0 +1,5 @@
#!/bin/bash
if [ ! -d "$HOME/.oh-my-zsh" ]; then
cp -r "/bootstrap/.oh-my-zsh" "$HOME/.oh-my-zsh"
fi

View File

@@ -0,0 +1,7 @@
#!/bin/bash
ZSH_FILE="$HOME/.zshrc"
if [ ! -f "$HOME/.zshrc" ]; then
cp "/bootstrap/.zshrc" "$HOME/.zshrc"
fi

View File

@@ -0,0 +1,17 @@
#!/bin/bash
if [ -d /bootstrap/entrypoint.d ]; then
for i in /bootstrap/entrypoint.d/*.sh; do
if [ -r $i ]; then
. $i
fi
done
unset i
fi
if [ ! -t 1 ] ; then
echo "No tty available."
exit 0
fi
exec "$@"

View File

@@ -0,0 +1,154 @@
{
"name": "phraseanet/phraseanet",
"description": "Phraseanet",
"license": "GPL-3.0",
"config": {
"bin-dir": "bin/"
},
"repositories": [
{
"type": "package",
"package": {
"name": "facebook/php-sdk",
"version": "3.2.3",
"source": {
"url": "https://github.com/facebookarchive/facebook-php-sdk.git",
"type": "git",
"reference": "3.2.3"
}
}
},
{
"type": "package",
"package": {
"name": "exiftool/exiftool",
"version": "11",
"source": {
"url": "https://github.com/exiftool/exiftool.git",
"type": "git",
"reference": "11.84"
}
}
},
{
"type": "vcs",
"url": "https://github.com/alchemy-fr/tcpdf-clone"
},
{
"type": "git",
"url": "https://github.com/romainneutron/ProcessManager.git"
},
{
"type": "vcs",
"url": "https://github.com/alchemy-fr/imagine"
},
{
"type": "vcs",
"url": "https://github.com/alchemy-fr/JMSTranslationBundle"
},
{
"type": "vcs",
"url": "https://github.com/alchemy-fr/embed-bundle.git"
},
{
"type": "git",
"url": "https://github.com/alchemy-fr/fractal.git"
}
],
"require": {
"php": ">=5.5.9",
"ext-intl": "*",
"alchemy-fr/tcpdf-clone": "~6.0",
"alchemy/embed-bundle": "^2.0.7",
"alchemy/geonames-api-consumer": "~0.1.0",
"alchemy/mediavorus": "^0.4.4",
"alchemy/oauth2php": "1.1.0",
"alchemy/phlickr": "0.2.9",
"alchemy/phpexiftool": "^0.7.0",
"alchemy/rest-bundle": "^0.0.5",
"alchemy/symfony-cors": "^0.1.0",
"alchemy/task-manager": "2.0.x-dev@dev",
"alchemy/zippy": "^0.3.0",
"beberlei/assert": "^2.3",
"cocur/slugify": "^2.0",
"dailymotion/sdk": "~1.5",
"data-uri/data-uri": "~0.1.0",
"dflydev/doctrine-orm-service-provider": "~1.0",
"doctrine/cache": "1.6.x-dev",
"doctrine/dbal": "^2.4.0",
"doctrine/migrations": "^1.0.0",
"doctrine/orm": "^2.4.0",
"elasticsearch/elasticsearch": "~2.0",
"firebase/php-jwt": "^3.0.0",
"gedmo/doctrine-extensions": "~2.3.0",
"goodby/csv": "^1.3.0",
"google/apiclient": "^2.0",
"guzzle/guzzle": "~3.0",
"hoa/compiler": "~2.0",
"hoa/console": "~2.0",
"hoa/dispatcher": "~0.0",
"hoa/router": "~2.0",
"igorw/get-in": "~1.0",
"imagine/imagine": "0.6.x-dev",
"jms/serializer": "~0.10",
"jms/translation-bundle": "dev-rebase-2015-10-20",
"justinrainbow/json-schema": "2.0.3 as 1.6.1",
"league/flysystem": "^1.0",
"league/flysystem-aws-s3-v2": "^1.0",
"league/fractal": "dev-webgalleries#af1acc0275438571bc8c1d08a05a4b5af92c9f97 as 0.13.0",
"media-alchemyst/media-alchemyst": "^0.5.5",
"monolog/monolog": "~1.3",
"mrclay/minify": "~2.1.6",
"neutron/process-manager": "2.0.x-dev@dev",
"neutron/recaptcha": "~0.1.0",
"neutron/silex-filesystem-provider": "~1.0",
"neutron/silex-imagine-provider": "~0.1.0",
"neutron/temporary-filesystem": "~2.1",
"pagerfanta/pagerfanta": "^1.0",
"php-ffmpeg/php-ffmpeg": "~0.5.0",
"php-xpdf/php-xpdf": "~0.2.1",
"exiftool/exiftool": "^11",
"ramsey/uuid": "^3.0",
"roave/security-advisories": "dev-master",
"silex/silex": "^1.3.0",
"silex/web-profiler": "~1.0",
"simple-bus/doctrine-orm-bridge": "^4.0",
"simple-bus/jms-serializer-bridge": "^1.0",
"simple-bus/message-bus": "^2.1",
"simple-bus/serialization": "^2.0",
"sorien/silex-dbal-profiler": "^1.1",
"sorien/silex-pimple-dumper": "^1.0",
"swiftmailer/swiftmailer": "~5.4.5",
"symfony/symfony": "~2.7.10|~2.8.3",
"themattharris/tmhoauth": "~0.7",
"twig/extensions": "^1.2.0",
"twig/twig": "~1.14, >=1.14.2",
"vierbergenlars/php-semver": "~2.1",
"webmozart/json": "^1.1",
"willdurand/negotiation": "^2.0.0-alpha1",
"zend/gdata": "~1.12.1",
"alchemy/worker-bundle": "^0.1.6",
"alchemy/queue-bundle": "^0.1.5",
"google/recaptcha": "^1.1",
"facebook/graph-sdk": "^5.6",
"box/spout": "^2.7",
"paragonie/random-lib": "^2.0",
"czproject/git-php": "^3.17"
},
"require-dev": {
"mikey179/vfsstream": "~1.5",
"phpunit/phpunit": "^4.8|^5.0"
},
"autoload": {
"psr-0": {
"Alchemy\\": "lib",
"": "lib/classes"
}
},
"include-path": ["vendor/zend/gdata/library"],
"extra": {
"branch-alias": {
"dev-master": "4.1.x-dev"
}
}
}

8839
docker/caching/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
docker/db/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM mariadb:10.4.5
RUN apt-get update && \
apt-get install -y \
gettext \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/
ADD ./docker/ /
ENTRYPOINT ["/phraseanet-entrypoint.sh"]
CMD ["mysqld","--sql_mode="]

View File

@@ -0,0 +1,3 @@
[mysqld]
max_allowed_packet=$MYSQL_MAX_ALLOWED_PACKET

View File

@@ -0,0 +1,5 @@
CREATE DATABASE ab_master CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE db_databox1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE db_unitTest CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE db_dataset1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE db_dataset2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

View File

@@ -0,0 +1,7 @@
#!/bin/bash
set -eo pipefail
shopt -s nullglob
envsubst < /custom.cnf > /etc/mysql/conf.d/custom.cnf
docker-entrypoint.sh "$@"

View File

@@ -0,0 +1,3 @@
FROM elasticsearch:2.4
RUN /usr/share/elasticsearch/bin/plugin install analysis-icu

View File

@@ -1,4 +0,0 @@
#!/bin/bash
cat nginx.conf.sample | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" > /etc/nginx/nginx.conf
nginx -g "daemon off;"

View File

@@ -1,85 +0,0 @@
user app;
worker_processes auto;
#error_log /var/log/ngnix_error.log info;
error_log /dev/stdout info;
pid /var/run/nginx.pid;
#daemon off;
events {
worker_connections 1024;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
reset_timedout_connection on;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
fastcgi_send_timeout 300s;
fastcgi_read_timeout 300;
resolver 127.0.0.11;
upstream backend {
server phraseanet:9000;
}
server {
listen 80;
server_name localhost;
error_log on;
access_log on;
root /var/alchemy/Phraseanet/www;
index index.php;
client_max_body_size $MAX_BODY_SIZE;
location /api {
rewrite ^(.*)$ /api.php/$1 last;
}
location / {
# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ @rewriteapp;
}
location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}
# PHP scripts -> PHP-FPM server listening on 127.0.0.1:9000
location ~ ^/(index|index_dev|api)\.php(/|$) {
fastcgi_pass backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ ^/(status|ping)$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_pass backend;
}
}
}

View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -xe
cat /nginx.conf.sample | sed "s/\$MAX_BODY_SIZE/$MAX_BODY_SIZE/g" > /etc/nginx/conf.d/default.conf
exec "$@"

View File

@@ -0,0 +1,31 @@
user app;
worker_processes 1;
error_log /var/log/ngnix_error.log info;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -0,0 +1,39 @@
upstream backend {
server phraseanet:9000;
}
server {
listen 80;
root /var/alchemy/Phraseanet/www;
index index.php;
client_max_body_size $MAX_BODY_SIZE;
location /api {
rewrite ^(.*)$ /api.php/$1 last;
}
location / {
# First attempt to serve request as file, then
# as directory, then fall back to index.html
try_files $uri $uri/ @rewriteapp;
}
location @rewriteapp {
rewrite ^(.*)$ /index.php/$1 last;
}
# PHP scripts -> PHP-FPM server listening on 127.0.0.1:9000
location ~ ^/(index|index_dev|api)\.php(/|$) {
fastcgi_pass backend;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ ^/(status|ping)$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_pass backend;
}
}

View File

@@ -1,9 +0,0 @@
#!/bin/sh
set -e
if [ ! -z ${DOCKER_XDEBUG_ENABLED} ]; then
. usr-bin/docker-xdebug-enable
fi
exec "$@"

View File

@@ -1,4 +0,0 @@
#!/bin/sh
/sbin/ip route|awk '/default/ { print $3 }'
# TODO support MacOS & Windows host IP

View File

@@ -1,5 +0,0 @@
#!/bin/sh
unset HOST_IP
unset XDEBUG_CONFIG
unset XDEBUG_REMOTE_HOST

View File

@@ -1,9 +0,0 @@
#!/bin/sh
set -e
HOST_IP=$(docker-get-host-ip);
export HOST_IP
export XDEBUG_CONFIG="remote_enable=1 remote_host=${HOST_IP} idekey=PHPSTORM";
export XDEBUG_REMOTE_HOST="${HOST_IP}";

View File

@@ -2,6 +2,16 @@
set -xe
if [ -z "$INSTALL_ACCOUNT_EMAIL" ]; then
echo "INSTALL_ACCOUNT_EMAIL var is not set."
exit 1
fi
if [ -z "$INSTALL_ACCOUNT_PASSWORD" ]; then
echo "INSTALL_ACCOUNT_PASSWORD var is not set."
exit 1
fi
/var/alchemy/Phraseanet/bin/setup system:install \
--email=$INSTALL_ACCOUNT_EMAIL \
--password=$INSTALL_ACCOUNT_PASSWORD \
@@ -17,28 +27,33 @@ set -xe
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.host elasticsearch
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.minScore 2
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.base_aggregate_limit 10
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.collection_aggregate_limit 10
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.doctype_aggregate_limit 10
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.minScore 2
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.facets._base.limit 10
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.facets._collection.limit 10
/var/alchemy/Phraseanet/bin/setup system:config set main.search-engine.options.facets._doctype.limit 10
## Redis
/var/alchemy/Phraseanet/bin/setup system:config set main.cache.options.host redis
/var/alchemy/Phraseanet/bin/setup system:config set main.cache.options.port 6379
/var/alchemy/Phraseanet/bin/setup system:config set main.cache.options.domain $INSTALL_SERVER_NAME
/var/alchemy/Phraseanet/bin/setup system:config set main.cache.options.namespace $INSTALL_SERVER_NAME
/var/alchemy/Phraseanet/bin/setup system:config set main.cache.type redis
# RabbitMQ
bin/setup system:config set rabbitmq.server.host rabbitmq
bin/setup system:config set rabbitmq.server.port 5672
bin/setup system:config set rabbitmq.server.user $INSTALL_RABBITMQ_USER
bin/setup system:config set rabbitmq.server.password $INSTALL_RABBITMQ_PASSWORD
bin/setup system:config set rabbitmq.server.vhost /
# Bus configuration for scheduler & worker
bin/setup system:config set workers.queue.worker-queue.registry alchemy_worker.queue_registry
bin/setup system:config set workers.queue.worker-queue.host rabbitmq
bin/setup system:config set workers.queue.worker-queue.port 5672
bin/setup system:config set workers.queue.worker-queue.user $INSTALL_RABBITMQ_USER
bin/setup system:config set workers.queue.worker-queue.password $INSTALL_RABBITMQ_PASSWORD
bin/setup system:config set workers.queue.worker-queue.vhost /
# Create elasticsearch index
/var/alchemy/Phraseanet/bin/console searchengine:index -c
## enable API and disable ssl on it
/var/alchemy/Phraseanet/bin/setup system:config set registry.api-clients.api-enabled true
/var/alchemy/Phraseanet/bin/setup system:config set main.api_require_ssl false
# set instance title
bin/setup system:config set registry.general.title $PHRASEANET_PROJECT_NAME
/var/alchemy/Phraseanet/bin/console compile:configuration

View File

@@ -1,11 +0,0 @@
#!/bin/bash
FILE=/var/alchemy/Phraseanet/config/configuration.yml
if [ -f "$FILE" ]; then
echo "$FILE exist, skip setup."
else
echo "$FILE doesn't exist, entering setup..."
runuser app -c '/auto-install.sh'
fi
php-fpm

35
docker/phraseanet/entrypoint.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
set -e
envsubst < "docker/phraseanet/php.ini.sample" > /usr/local/etc/php/php.ini
envsubst < "docker/phraseanet/php-fpm.conf.sample" > /usr/local/etc/php-fpm.conf
envsubst < "docker/phraseanet/root/usr/local/etc/php-fpm.d/zz-docker.conf" > /usr/local/etc/php-fpm.d/zz-docker.conf
chown -R app:app \
cache \
config \
datas \
tmp \
logs \
www/thumbnails \
www/custom
FILE=config/configuration.yml
if [ -f "$FILE" ]; then
bin/setup system:config set registry.general.title $PHRASEANET_PROJECT_NAME
echo "$FILE exists, skip setup."
else
echo "$FILE doesn't exist, entering setup..."
runuser app -c docker/phraseanet/auto-install.sh
fi
if [ ${XDEBUG_ENABLED} == "1" ]; then
echo "XDEBUG is enabled. YOU MAY KEEP THIS FEATURE DISABLED IN PRODUCTION."
docker-php-ext-enable xdebug
fi
./docker/phraseanet/plugins/console init
bash -e docker-php-entrypoint $@

View File

@@ -935,7 +935,7 @@ cli_server.color = On
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
date.timezone = Europe/Paris
date.timezone = UTC
; http://php.net/date.default-latitude
;date.default_latitude = 31.7667

View File

@@ -1,8 +0,0 @@
#!/bin/bash
set -e
envsubst < /php.ini.sample > /usr/local/etc/php/php.ini
envsubst < /php-fpm.conf.sample > /usr/local/etc/php-fpm.conf
docker-php-entrypoint $@

View File

@@ -0,0 +1,29 @@
<?php
namespace Alchemy\Docker\Plugins\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class InitCommand extends Command
{
protected function configure()
{
$this
->setName('init')
->setDescription('Initialize plugins');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
foreach (glob('./plugins/*') as $dir) {
if (is_dir($dir)) {
$output->writeln(sprintf('Init <info>%s</info> plugin', basename($dir)));
SubCommand::run(sprintf('bin/setup plugin:add %s', $dir));
}
}
return 0;
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Alchemy\Docker\Plugins\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class InstallCommand extends Command
{
protected function configure()
{
$this
->setName('install')
->setDescription('Install plugins');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$plugins = trim(getenv('PHRASEANET_PLUGINS'));
if (empty($plugins)) {
$output->writeln('<comment>No plugin to install... SKIP</comment>');
return 0;
}
$pluginsDir = 'plugins';
if (!is_dir($pluginsDir)) {
mkdir($pluginsDir);
}
foreach (explode(' ', $plugins) as $key => $plugin) {
$plugin = trim($plugin);
$repo = $plugin;
$branch = 'master';
if (1 === preg_match('#^(.+)\(([^)]+)\)$#', $plugin, $matches)) {
$repo = $matches[1];
$branch = $matches[2];
}
$pluginPath = './plugin' . $key;
if (is_dir($pluginPath)) {
SubCommand::run(sprintf('rm -rf %s', $pluginPath));
}
$output->writeln(sprintf('Installing <info>%s</info> (branch: <info>%s</info>)', $repo, $branch));
SubCommand::run(sprintf('git clone --single-branch --branch %s %s %s', $branch, $repo, $pluginPath));
$manifestSrc = $pluginPath.'/manifest.json';
if (!file_exists($manifestSrc)) {
throw new \Exception(sprintf('Cannot install plugin %s: no manifest.json file found', $plugin));
}
$pluginDestName = json_decode(file_get_contents($manifestSrc), true)['name'];
rename($pluginPath, $pluginsDir.'/'.$pluginDestName);
$pluginPath = $pluginsDir.'/'.$pluginDestName;
if (file_exists($pluginPath.'/composer.json')) {
SubCommand::run(sprintf('cd %s && composer install --no-dev', $pluginPath));
}
}
return 0;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Alchemy\Docker\Plugins\Command;
function setupStreaming()
{
ini_set('output_buffering', 'off');
ini_set('zlib.output_compression', false);
if (function_exists('apache_setenv')) {
apache_setenv('no-gzip', '1');
apache_setenv('dont-vary', '1');
}
}
setupStreaming();
abstract class SubCommand
{
static public function run($cmd)
{
system($cmd, $return);
if (0 !== $return) {
throw new \Exception(sprintf('Error %d: %s', $return, $cmd));
}
}
}

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env php
<?php
namespace Alchemy\Docker\Plugins\Command;
require __DIR__.'/../../../vendor/autoload.php';
require __DIR__.'/SubCommand.php';
require __DIR__.'/InstallCommand.php';
require __DIR__.'/InitCommand.php';
use Symfony\Component\Console\Application;
$application = new Application();
$application->add(new InstallCommand());
$application->add(new InitCommand());
$application->run();

View File

@@ -1,6 +1,5 @@
[global]
daemonize = no
error_log = /var/log/fpm-php.www.log
process.max = 128
[www]

View File

@@ -1,3 +0,0 @@
#!/bin/bash
runuser app -c 'php /var/alchemy/Phraseanet/bin/console task-manager:scheduler:run'

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
DOCKER_DIR="./docker/phraseanet"
mkdir -p "${APP_DIR}/tmp/locks" \
&& chown -R app:app "${APP_DIR}/tmp"
envsubst < "${DOCKER_DIR}/php.ini.sample" > /usr/local/etc/php/php.ini
envsubst < "${DOCKER_DIR}/php-fpm.conf.sample" > /usr/local/etc/php-fpm.conf
if [ ${XDEBUG_ENABLED} == "1" ]; then
echo "XDEBUG is enabled. YOU MAY KEEP THIS FEATURE DISABLED IN PRODUCTION."
docker-php-ext-enable xdebug
fi
runuser -u app -- $@

View File

@@ -128,7 +128,7 @@ key:
| quoted_string()
group:
::space::? ::parenthese_:: primary() ::_parenthese:: ::space::?
::space::? ::parenthese_:: ::space::? primary() ::space::? ::_parenthese:: ::space::?
// Thesaurus terms

View File

@@ -167,11 +167,11 @@ class AccountService
* @param string $login
* @throws AccountException
*/
public function deleteAccount($login = null)
public function deleteAccount($login = null, array $grantedBaseIdList = array())
{
$user = $this->getUserOrCurrentUser($login);
$this->userManipulator->delete($user);
$this->userManipulator->delete($user, $grantedBaseIdList);
}
/**

View File

@@ -15,6 +15,7 @@ use Alchemy\Phrasea\Controller\Api\Result;
use Alchemy\Phrasea\ControllerProvider\Api\OAuth2;
use Alchemy\Phrasea\ControllerProvider\Api\V1;
use Alchemy\Phrasea\ControllerProvider\Api\V2;
use Alchemy\Phrasea\ControllerProvider\Api\V3;
use Alchemy\Phrasea\ControllerProvider\Datafiles;
use Alchemy\Phrasea\ControllerProvider\MediaAccessor;
use Alchemy\Phrasea\ControllerProvider\Minifier;
@@ -36,6 +37,7 @@ class ApiApplicationLoader extends BaseApplicationLoader
$app->register(new OAuth2());
$app->register(new V1());
$app->register(new V2());
$app->register(new V3());
$app->register(new ApiReportControllerProvider());
$app->register(new JsonSchemaServiceProvider());
}
@@ -119,6 +121,16 @@ class ApiApplicationLoader extends BaseApplicationLoader
'access_token' => '/api/oauthv2/token'
],
],
'3' => [
'number' => V3::VERSION,
'uri' => '/api/v3/',
'authenticationProtocol' => 'OAuth2',
'authenticationVersion' => 'draft#v9',
'authenticationEndPoints' => [
'authorization_token' => '/api/oauthv2/authorize',
'access_token' => '/api/oauthv2/token'
]
],
]
])->createResponse();
});
@@ -135,6 +147,7 @@ class ApiApplicationLoader extends BaseApplicationLoader
$app->mount('/datafiles/', new Datafiles());
$app->mount('/api/v1', new V1());
$app->mount('/api/v2', new V2());
$app->mount('/api/v3', new V3());
$app->mount('/api/report', new ApiReportControllerProvider());
$app->mount('/permalink/', new Permalink());
$app->mount($app['controller.media_accessor.route_prefix'], new MediaAccessor());

View File

@@ -328,11 +328,9 @@ class RegistrationService
$autoReg = $acl->get_granted_base();
$granted = [];
foreach ($autoReg as $baseId => $collection) {
$granted[$baseId] = $collection->get_label($this->app['locale']);
}
if(count($granted) > 0) {
$this->app['manipulator.webhook-event']->create(
WebhookEvent::USER_REGISTRATION_GRANTED,
WebhookEvent::USER_REGISTRATION_TYPE,
@@ -340,8 +338,11 @@ class RegistrationService
'user_id' => $user->getId(),
'granted' => $granted,
'rejected' => []
]
],
[$baseId]
);
unset($granted);
}

View File

@@ -34,6 +34,11 @@ abstract class AbstractChecker implements CheckerInterface
*/
protected $collections = [];
/**
* @var \collection[]
*/
protected $compareIgnoreCollections = [];
public function __construct(Application $app)
{
$this->app = $app;
@@ -44,7 +49,7 @@ abstract class AbstractChecker implements CheckerInterface
* Warning, you can not restrict on both databoxes and collections
*
* @param \databox[] $databoxes A databox or an array of databoxes
* @return bool
* @return \databox[]
*
* @throws \LogicException If already restricted to collections
* @throws \InvalidArgumentException In case invalid databoxes are provided
@@ -72,7 +77,7 @@ abstract class AbstractChecker implements CheckerInterface
* Warning, you can not restrict on both databoxes and collections
*
* @param \collection[] $collections
* @return bool
* @return \collection[]
*
* @throws \LogicException If already restricted to databoxes
* @throws \InvalidArgumentException In case invalid collections are provided
@@ -95,6 +100,11 @@ abstract class AbstractChecker implements CheckerInterface
return $this->collections;
}
public function setCompareIgnoreCollections($collections)
{
$this->compareIgnoreCollections = $collections;
}
/**
* Returns true if the checker should be executed against the current file
*

View File

@@ -45,8 +45,18 @@ class Filename extends AbstractChecker
*/
public function check(EntityManager $em, File $file)
{
$boolean = empty(\record_adapter::get_records_by_originalname(
$file->getCollection()->get_databox(), $file->getOriginalName(), $this->sensitive, 0, 1
$excludedCollIds = [];
if (!empty($this->compareIgnoreCollections)) {
foreach ($this->compareIgnoreCollections as $collection) {
// use only collection in the same databox and retrieve the coll_id
if ($collection->get_sbas_id() === $file->getCollection()->get_sbas_id()) {
$excludedCollIds[] = $collection->get_coll_id();
}
}
}
$boolean = empty(\record_adapter::getRecordsByOriginalnameWithExcludedCollIds(
$file->getCollection()->get_databox(), $file->getOriginalName(), $this->sensitive, 0, 1, $excludedCollIds
));
return new Response($boolean, $this);

View File

@@ -34,7 +34,17 @@ class Sha256 extends AbstractChecker
*/
public function check(EntityManager $em, File $file)
{
$boolean = empty($file->getCollection()->get_databox()->getRecordRepository()->findBySha256($file->getSha256()));
$excludedCollIds = [];
if (!empty($this->compareIgnoreCollections)) {
foreach ($this->compareIgnoreCollections as $collection) {
// use only collection in the same databox and retrieve the coll_id
if ($collection->get_sbas_id() === $file->getCollection()->get_sbas_id()) {
$excludedCollIds[] = $collection->get_coll_id();
}
}
}
$boolean = empty($file->getCollection()->get_databox()->getRecordRepository()->findBySha256WithExcludedCollIds($file->getSha256(), $excludedCollIds));
return new Response($boolean, $this);
}

View File

@@ -33,7 +33,17 @@ class UUID extends AbstractChecker
*/
public function check(EntityManager $em, File $file)
{
$boolean = empty($file->getCollection()->get_databox()->getRecordRepository()->findByUuid($file->getUUID()));
$excludedCollIds = [];
if (!empty($this->compareIgnoreCollections)) {
foreach ($this->compareIgnoreCollections as $collection) {
// use only collection in the same databox and retrieve the coll_id
if ($collection->get_sbas_id() === $file->getCollection()->get_sbas_id()) {
$excludedCollIds[] = $collection->get_coll_id();
}
}
}
$boolean = empty($file->getCollection()->get_databox()->getRecordRepository()->findByUuidWithExcludedCollIds($file->getUUID(), $excludedCollIds));
return new Response($boolean, $this);
}

View File

@@ -14,6 +14,8 @@ namespace Alchemy\Phrasea\Border;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Border\Checker\CheckerInterface;
use Alchemy\Phrasea\Border\Attribute\AttributeInterface;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Metadata\Tag\TfArchivedate;
use Alchemy\Phrasea\Metadata\Tag\TfQuarantine;
@@ -333,7 +335,7 @@ class Manager
$this->app['phraseanet.metadata-setter']->replaceMetadata($newMetadata, $element);
if(!$nosubdef) {
$element->rebuild_subdefs();
$this->app['dispatcher']->dispatch(RecordEvents::SUBDEFINITION_CREATE, new SubdefinitionCreateEvent($element, true));
}
return $element;

View File

@@ -73,7 +73,7 @@ class RedisCache extends CacheProvider implements Cache
*/
protected function doDelete($id)
{
return $this->_redis->delete($id);
return $this->_redis->del($id);
}
/**

View File

@@ -0,0 +1,89 @@
<?php
/**
* This file is part of Phraseanet
*
* (c) 2005-2020 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Alchemy\Phrasea\Model\Entities\User;
class ApplyRightsCommand extends Command
{
public function __construct($name = null)
{
parent::__construct($name);
$this->setDescription('Apply right on databox, inject appbox:basusr to dboxes:collusr')
->addOption('user_id', null, InputOption::VALUE_REQUIRED, 'the user ID to apply rights')
;
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$userId = $input->getOption('user_id');
$userRepository = $this->container['repo.users'];
if ($userId) {
if (($user = $userRepository->find($userId)) === null) {
$output->writeln('user not found!');
return 0;
}
$this->injectRightsSbas($user);
} else {
foreach ($userRepository->findAll() as $user) {
$this->injectRightsSbas($user);
}
}
$output->writeln('Apply right on databox finished!');
return 0;
}
private function injectRightsSbas(User $user)
{
$userAcl = $this->container->getAclForUser($user);
foreach ($userAcl->get_granted_sbas() as $databox) {
$userAcl->delete_injected_rights_sbas($databox);
$sql = "INSERT INTO collusr
(site, usr_id, coll_id, mask_and, mask_xor, ord)
VALUES (:site_id, :usr_id, :coll_id, :mask_and, :mask_xor, :ord)";
$stmt = $databox->get_connection()->prepare($sql);
$iord = 0;
// fix collusr if user has right on collection
foreach ($userAcl->get_granted_base([], [$databox->get_sbas_id()]) as $collection) {
try {
$stmt->execute([
':site_id' => $this->container['conf']->get(['main', 'key']),
':usr_id' => $user->getId(),
':coll_id' => $collection->get_coll_id(),
':mask_and' => $userAcl->get_mask_and($collection->get_base_id()),
':mask_xor' => $userAcl->get_mask_xor($collection->get_base_id()),
':ord' => $iord++
]);
} catch (DBALException $e) {
}
}
$stmt->closeCursor();
}
}
}

View File

@@ -0,0 +1,86 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Collection;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ListCollectionCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('collection:list');
$this->setDescription('List all collection in Phraseanet')
->addOption('databox_id', 'd', InputOption::VALUE_REQUIRED, 'The id of the databox to list collection')
->addOption('jsonformat', null, InputOption::VALUE_NONE, 'Output in json format')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
try {
$jsonformat = $input->getOption('jsonformat');
$databox = $this->container->findDataboxById($input->getOption('databox_id'));
$collections = $this->listDataboxCollections($databox);
if ($jsonformat) {
foreach ($collections as $collection) {
$collectionList[] = array_combine(['id local for API', 'id distant', 'name','label','status','total records'], $collection);
}
echo json_encode($collectionList);
} else {
$table = $this->getHelperSet()->get('table');
$table
->setHeaders(['id local for API', 'id distant', 'name','label','status','total records'])
->setRows($collections)
->render($output);
}
} catch (\Exception $e) {
$output->writeln("<error>{$e->getMessage()}</error>");
}
return 0;
}
private function listDataboxCollections(\databox $databox)
{
return array_map(function (\collection $collection) {
return $this->listCollection($collection);
}, array_merge($databox->get_collections(),$this->getUnabledCollection($databox->get_activable_colls())));
}
private function getUnabledCollection($collections)
{
return array_map(function ($colId){
return \collection::getByBaseId($this->container, $colId);
},$collections);
}
private function listCollection(\collection $collection)
{
return [
$collection->get_base_id(),
$collection->get_coll_id(),
$collection->get_name(),
'en: ' . $collection->get_label('en') .
', de: ' . $collection->get_label('de') .
', fr: ' . $collection->get_label('fr') .
', nl: ' . $collection->get_label('nl'),
($collection->is_active()) ? 'enabled' : 'disabled',
$collection->get_record_amount()
];
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Collection;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class PublishCollectionCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('collection:publish');
$this->setDescription('Publish collection in Phraseanet')
->addOption('collection_id', null, InputOption::VALUE_REQUIRED, 'The base_id of the collection to publish but keep with existing right into present in application box.')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
try {
$collection = \collection::getByBaseId($this->container,(int)$input->getOption('collection_id'));
$dialog = $this->getHelperSet()->get('dialog');
do {
$continue = mb_strtolower($dialog->ask($output, '<question> Do you want really publish this collection? (y/N)</question>', 'N'));
} while ( ! in_array($continue, ['y', 'n']));
if ($continue !== 'y') {
$output->writeln('Aborting !');
return;
}
$collection->enable($this->container->getApplicationBox());
$output->writeln('<info>Publish collection successful</info>');
} catch (\Exception $e) {
$output->writeln('<error>Publish collection failed : '.$e->getMessage().'</error>');
}
return 0;
}
}

View File

@@ -0,0 +1,61 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Collection;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class UnPublishCollectionCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('collection:unpublish');
$this->setDescription('Unpublish collection in Phraseanet')
->addOption('collection_id', null, InputOption::VALUE_REQUIRED, 'The base_id of the collection to unpublish, the base_id is the same id used in API.')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
try {
$collection = \collection::getByBaseId($this->container,(int)$input->getOption('collection_id'));
$dialog = $this->getHelperSet()->get('dialog');
do {
$continue = mb_strtolower($dialog->ask($output, sprintf("<question> Do you want really unpublish the collection %s? (y/N)</question>", $collection->get_name()), 'N'));
} while ( ! in_array($continue, ['y', 'n']));
if ($continue !== 'y') {
$output->writeln('<info>Aborting !</>');
return;
}
$collection->disable($this->container->getApplicationBox());
$output->writeln('<info>Unpublish collection successful</info>');
} catch (\Exception $e) {
$output->writeln('<error>Unpublish collection failed : '.$e->getMessage().'</error>');
}
return 0;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Databox;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ListDataboxCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('databox:list');
$this->setDescription('List all databox in Phraseanet')
->addOption('jsonformat', null, InputOption::VALUE_NONE, 'Output in json format')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
try {
$jsonformat = $input->getOption('jsonformat');
$databoxes = array_map(function (\databox $databox) {
return $this->listDatabox($databox);
}, $this->container->getApplicationBox()->get_databoxes());
if ($jsonformat) {
foreach ($databoxes as $databox) {
$databoxList[] = array_combine(['id', 'name', 'alias'], $databox);
}
echo json_encode($databoxList);
} else {
$table = $this->getHelperSet()->get('table');
$table
->setHeaders(['id', 'name', 'alias'])
->setRows($databoxes)
->render($output);
}
} catch (\Exception $e) {
$output->writeln('<error>Listing databox failed : '.$e->getMessage().'</error>');
}
return 0;
}
private function listDatabox(\databox $databox)
{
return [
$databox->get_sbas_id(),
$databox->get_dbname(),
$databox->get_viewname()
];
}
}

View File

@@ -1,7 +1,16 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2020 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Databox;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Databox\DataboxConnectionSettings;
use Alchemy\Phrasea\Databox\DataboxService;
@@ -13,42 +22,84 @@ use Symfony\Component\Console\Output\OutputInterface;
class MountDataboxCommand extends Command
{
protected function configure()
/**
* Constructor
*/
public function __construct($name = null)
{
$this->setName('databox:mount')
->addArgument('databox', InputArgument::REQUIRED, 'Database name for the databox', null)
->addArgument('owner', InputArgument::REQUIRED, 'Email of the databox admin user', null)
->addOption('connection', 'c', InputOption::VALUE_NONE, 'Flag to set new database settings')
->addOption('db-host', null, InputOption::VALUE_OPTIONAL, 'MySQL server host', 'localhost')
->addOption('db-port', null, InputOption::VALUE_OPTIONAL, 'MySQL server port', 3306)
->addOption('db-user', null, InputOption::VALUE_OPTIONAL, 'MySQL server user', 'phrasea')
->addOption('db-password', null, InputOption::VALUE_OPTIONAL, 'MySQL server password', null);
parent::__construct('databox:mount');
$this->setDescription('Mount databox')
->addArgument('databox', InputArgument::REQUIRED, 'Database name in Mysql', null)
->addArgument('user_id', InputArgument::REQUIRED, 'The Id of user owner (this account became full admin on this databox)', null)
->addOption('db-host', null, InputOption::VALUE_OPTIONAL, 'MySQL server host')
->addOption('db-port', null, InputOption::VALUE_OPTIONAL, 'MySQL server port')
->addOption('db-user', null, InputOption::VALUE_OPTIONAL, 'MySQL server user')
->addOption('db-password', null, InputOption::VALUE_OPTIONAL, 'MySQL server password')
;
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$databoxName = $input->getArgument('databox');
$connectionSettings = $input->getOption('connection') == false ? null : new DataboxConnectionSettings(
$input->getOption('db-host'),
$input->getOption('db-port'),
$input->getOption('db-user'),
$input->getOption('db-password')
);
try {
/** @var UserRepository $userRepository */
$userRepository = $this->container['repo.users'];
$owner = $userRepository->find($input->getArgument('user_id'));
if (empty($owner)) {
$output->writeln('<error>User not found ! </error>');
return;
}
if ($owner->isGuest() || !$this->container->getAclForUser($owner)->is_admin()) {
$output->writeln('<error>Admin role is required for the owner ! </error>');
return;
}
$databoxName = $input->getArgument('databox');
$dialog = $this->getHelperSet()->get('dialog');
$connectionSettings = new DataboxConnectionSettings(
$input->getOption('db-host')?:$this->container['conf']->get(['main', 'database', 'host']),
$input->getOption('db-port')?:$this->container['conf']->get(['main', 'database', 'port']),
$input->getOption('db-user')?:$this->container['conf']->get(['main', 'database', 'user']),
$input->getOption('db-password')?:$this->container['conf']->get(['main', 'database', 'password'])
);
do {
$continue = mb_strtolower($dialog->ask($output, '<question> Do you want really mount this databox? (y/N)</question>', 'N'));
}
while ( ! in_array($continue, ['y', 'n']));
if ($continue !== 'y') {
$output->writeln('Aborting !');
return;
}
/** @var DataboxService $databoxService */
$databoxService = $this->container['databox.service'];
$owner = $userRepository->findByEmail($input->getArgument('owner'));
\phrasea::clear_sbas_params($this->container);
$databoxService->mountDatabox(
$databox = $databoxService->mountDatabox(
$databoxName,
$owner,
$connectionSettings
);
$output->writeln('Databox mounted');
$output->writeln("\n\t<info>Data-Box ID ".$databox->get_sbas_id()." mounted successful !</info>\n");
} catch (\Exception $e) {
$output->writeln('<error>Mount databox failed :'.$e->getMessage().'</error>');
}
return 0;
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\Databox;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UnMountDataboxCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('databox:unmount');
$this->setDescription('Unmount databox')
->addArgument('databox_id', InputArgument::REQUIRED, 'The id of the databox to unmount', null)
;
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
try {
$databox = $this->container->findDataboxById($input->getArgument('databox_id'));
$dialog = $this->getHelperSet()->get('dialog');
do {
$continue = mb_strtolower($dialog->ask($output, '<question> Do you want really unmount this databox? (y/N)</question>', 'N'));
} while ( ! in_array($continue, ['y', 'n']));
if ($continue !== 'y') {
$output->writeln('Aborting !');
return;
}
$databox->unmount_databox();
$output->writeln('<info>Unmount databox successful</info>');
} catch (\Exception $e) {
$output->writeln('<error>Unmount databox failed : '.$e->getMessage().'</error>');
}
return 0;
}
}

View File

@@ -15,6 +15,7 @@ use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Plugin\Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\HttpKernel\Client;
@@ -30,6 +31,15 @@ class JsFixtures extends Command
protected function doExecute(InputInterface $input, OutputInterface $output)
{
/** @var Plugin $plugin */
$msg = [];
foreach($this->container['plugins.manager']->listPlugins() as $plugin) {
$msg[] = sprintf(" bin/setup plugins:remove \"%s\"", $plugin->getName());
}
if(count($msg) !== 0) {
throw new RuntimeException("You must remove plugins first:\n" . join("\n", $msg));
}
if (!file_exists($this->container['db.fixture.info']['path'])) {
throw new RuntimeException('You must generate sqlite db first, run "bin/developer phraseanet:regenerate-sqlite" command.');
}
@@ -104,6 +114,7 @@ class JsFixtures extends Command
private function writeResponse(OutputInterface $output, $method, $path, $to, $authenticateUser = false)
{
$environment = Application::ENV_TEST;
/** @var Application $app */
$app = require __DIR__ . '/../../Application/Root.php';
$app['orm.em'] = $app->extend('orm.em', function($em, $app) {
return $app['orm.ems'][$app['db.fixture.hash.key']];

View File

@@ -15,6 +15,21 @@ use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
function normalizePath($path) {
return array_reduce(explode('/', $path), function ($a, $b) {
if($a === 0)
$a = '/';
if($b === '' || $b === '.')
return $a;
if($b === '..')
return dirname($a);
return preg_replace('/\/+/', '/', "$a/$b");
}, 0);
}
abstract class AbstractPluginCommand extends Command
{
protected function validatePlugins(InputInterface $input, OutputInterface $output)
@@ -54,33 +69,42 @@ abstract class AbstractPluginCommand extends Command
protected function doInstallPlugin($source, InputInterface $input, OutputInterface $output)
{
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
$output->write("Importing <info>$source</info>...");
$this->container['plugins.importer']->import($source, $temporaryDir);
$output->writeln(" <comment>OK</comment>");
$output->write("Validating plugin...");
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir);
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($source);
$output->writeln(" <comment>OK</comment> found <info>".$manifest->getName()."</info>");
$targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName();
$output->write("Setting up composer...");
$this->container['plugins.composer-installer']->install($temporaryDir);
if (normalizePath($targetDir) !== normalizePath($source)) {
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
$output->write("Importing <info>$source</info>...");
$this->container['plugins.importer']->import($source, $temporaryDir);
$output->writeln(" <comment>OK</comment>");
$workingDir = $temporaryDir;
} else {
$workingDir = $targetDir;
}
if (!is_dir($workingDir.'/vendor')) {
$output->write("Setting up composer...");
$this->container['plugins.composer-installer']->install($workingDir);
$output->writeln(" <comment>OK</comment>");
}
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
if (isset($temporaryDir)) {
$this->container['filesystem']->mirror($temporaryDir, $targetDir);
}
$output->writeln(" <comment>OK</comment>");
$output->write("Copying public files <info>".$manifest->getName()."</info>...");
$this->container['plugins.assets-manager']->update($manifest);
$output->writeln(" <comment>OK</comment>");
if (isset($temporaryDir)) {
$output->write("Removing temporary directory...");
$this->container['filesystem']->remove($temporaryDir);
$output->writeln(" <comment>OK</comment>");
}
$output->write("Activating plugin...");
$this->container['conf']->set(['plugins', $manifest->getName(), 'enabled'], true);

View File

@@ -13,6 +13,7 @@ namespace Alchemy\Phrasea\Command\Setup;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Core\Configuration\StructureTemplate;
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions;
use Doctrine\DBAL\Driver\Connection;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Input\ArrayInput;
@@ -51,8 +52,17 @@ class Install extends Command
->addOption('db-template', null, InputOption::VALUE_OPTIONAL, 'Databox template (' . $this->structureTemplate->toString() . ')', null)
->addOption('data-path', null, InputOption::VALUE_OPTIONAL, 'Path to data repository', realpath(__DIR__ . '/../../../../../datas'))
->addOption('server-name', null, InputOption::VALUE_OPTIONAL, 'Server name')
->addOption('indexer', null, InputOption::VALUE_OPTIONAL, 'Path to Phraseanet Indexer', 'auto')
->addOption('yes', 'y', InputOption::VALUE_NONE, 'Answer yes to all questions');
->addOption('es-host', null, InputOption::VALUE_OPTIONAL, 'ElasticSearch server HTTP host', 'localhost')
->addOption('es-port', null, InputOption::VALUE_OPTIONAL, 'ElasticSearch server HTTP port', 9200)
->addOption('es-index', null, InputOption::VALUE_OPTIONAL, 'ElasticSearch index name', null)
->addOption('download-path', null, InputOption::VALUE_OPTIONAL, 'Path to download repository', __DIR__ . '/../../../../../tmp/download')
->addOption('lazaret-path', null, InputOption::VALUE_OPTIONAL, 'Path to lazaret repository', __DIR__ . '/../../../../../tmp/lazaret')
->addOption('caption-path', null, InputOption::VALUE_OPTIONAL, 'Path to caption repository', __DIR__ . '/../../../../../tmp/caption')
->addOption('scheduler-locks-path', null, InputOption::VALUE_OPTIONAL, 'Path to scheduler-locks repository', __DIR__ . '/../../../../../tmp/locks')
->addOption('worker-tmp-files', null, InputOption::VALUE_OPTIONAL, 'Path to worker-tmp-files repository', __DIR__ . '/../../../../../tmp')
->addOption('yes', 'y', InputOption::VALUE_NONE, 'Answer yes to all questions')
->setHelp("Phraseanet can only be installed on 64 bits PHP.");
;
return $this;
}
@@ -72,6 +82,14 @@ class Install extends Command
*/
protected function doExecute(InputInterface $input, OutputInterface $output)
{
if(PHP_INT_SIZE !== 8) {
$output->writeln(sprintf(
"<error>Phraseanet can only be installed on 64 bits PHP, your version is %d bits (PHP_INT_SIZE=%d).</error>",
PHP_INT_SIZE<<3,PHP_INT_SIZE
));
return -1;
}
/** @var DialogHelper $dialog */
$dialog = $this->getHelperSet()->get('dialog');
@@ -121,6 +139,21 @@ class Install extends Command
list($email, $password) = $this->getCredentials($input, $output, $dialog);
$dataPath = $this->getDataPath($input, $output, $dialog);
if (! $input->getOption('yes')) {
$output->writeln("<info>--- ElasticSearch connection settings ---</info>");
}
list($esHost, $esPort) = $this->getESHost($input, $output, $dialog);
$esIndexName = $this->getESIndexName($input, $output, $dialog);
$esOptions = ElasticsearchOptions::fromArray([
'host' => $esHost,
'port' => $esPort,
'index' => $esIndexName
]);
$output->writeln('');
if (!$input->getOption('yes')) {
$continue = $dialog->askConfirmation($output, "<question>Phraseanet is going to be installed, continue ? (N/y)</question>", false);
@@ -131,7 +164,10 @@ class Install extends Command
}
}
$this->container['phraseanet.installer']->install($email, $password, $abConn, $serverName, $dataPath, $dbConn, $templateName, $this->detectBinaries());
$storagePaths = $this->getStoragePaths($input, $dataPath);
$this->container['phraseanet.installer']->install($email, $password, $abConn, $serverName, $storagePaths, $dbConn, $templateName, $this->detectBinaries());
$this->container['conf']->set(['main', 'search-engine', 'options'], $esOptions->toArray());
if (null !== $this->getApplication()) {
$command = $this->getApplication()->find('crossdomain:generate');
@@ -339,6 +375,56 @@ class Install extends Command
return $serverName;
}
private function getESHost(InputInterface $input, OutputInterface $output, DialogHelper $dialog)
{
$host = $input->getOption('es-host');
$port = (int) $input->getOption('es-port');
if (! $input->getOption('yes')) {
while (! $host) {
$host = $dialog->ask($output, 'ElasticSearch server host : ', null);
};
while ($port <= 0 || $port >= 65535) {
$port = (int) $dialog->ask($output, 'ElasticSearch server port : ', null);
};
}
return [ $host, $port ];
}
private function getESIndexName(InputInterface $input, OutputInterface $output, DialogHelper $dialog)
{
$index = $input->getOption('es-index');
if (! $input->getOption('yes')) {
$index = $dialog->ask($output, 'ElasticSearch server index name (blank to autogenerate) : ', null);
}
return $index;
}
private function getStoragePaths(InputInterface $input, $dataPath)
{
$schedulerLocksPath = $input->getOption('scheduler-locks-path');
if (!is_dir($schedulerLocksPath)) {
mkdir($schedulerLocksPath, 0755, true);
}
if (($schedulerLocksPath = realpath($schedulerLocksPath)) === FALSE) {
throw new \InvalidArgumentException(sprintf('Path %s does not exist.', $schedulerLocksPath));
}
return [
'subdefs' => $dataPath,
'download' => $input->getOption('download-path'),
'lazaret' => $input->getOption('lazaret-path'),
'caption' => $input->getOption('caption-path'),
'worker_tmp_files' => $input->getOption('worker-tmp-files')
];
}
private function detectBinaries()
{
return [

View File

@@ -0,0 +1,329 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2020 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\User;
use Alchemy\Phrasea\Command\Command;
use Symfony\Component\Console\Helper\DialogHelper;
use Alchemy\Phrasea\ControllerProvider\Api\V2;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\Model\Entities\ApiApplication;
use Alchemy\Phrasea\Model\Entities\ApiAccount;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Notification\Mail\MailRequestPasswordSetup;
use Alchemy\Phrasea\Notification\Mail\MailRequestEmailConfirmation;
use Alchemy\Phrasea\Notification\Receiver;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class UserApplicationsCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('user:applications');
$this->setDescription('List, Create, Edit, Delete application in Phraseanet <comment>(experimental)</>')
->addOption('list', null, InputOption::VALUE_NONE, 'List all applications or user applications if --user_id is set')
->addOption('create', null, InputOption::VALUE_NONE, 'Create application for user in Phraseanet')
->addOption('edit', null, InputOption::VALUE_NONE, 'Edit application in Phraseanet work only if app_id is set')
->addOption('delete', null, InputOption::VALUE_NONE, 'Delete application in Phraseanet, require an app_id')
->addOption('user_id', 'u', InputOption::VALUE_REQUIRED, 'The Id of user owner of application (user_id), required to Create, Edit and Delete.')
->addOption('app_id', 'a', InputOption::VALUE_REQUIRED, 'The application ID, required for Edit and Delete')
->addOption('name', null, InputOption::VALUE_REQUIRED, 'The desired name for application, required for Create and Edit.')
->addOption('type', 't', InputOption::VALUE_OPTIONAL, 'The kind of application, Desktop or Web.',ApiApplication::WEB_TYPE)
->addOption('description', 'd', InputOption::VALUE_REQUIRED, 'The desired description for application.')
->addOption('website', 'w', InputOption::VALUE_OPTIONAL, 'The desired url, eg: -w "https://www.alchemy.fr".')
->addOption('callback', 'c', InputOption::VALUE_OPTIONAL, 'The desired endpoint for callback, required for web kind eg: -c "https://www.alchemy.fr/callback"')
->addOption('webhook_url', null, InputOption::VALUE_REQUIRED, 'The webhook url')
->addOption('active', null, InputOption::VALUE_OPTIONAL, 'Activate or deactivate the app, values true or false', 'true')
->addOption('generate_token', null, InputOption::VALUE_NONE, 'Generate or regenerate the access token')
->addOption('password_oauth2_gt', null, InputOption::VALUE_OPTIONAL, 'Activate or deactivate password OAuth2 grant type , values true or false', 'false')
->addOption('jsonformat', null, InputOption::VALUE_NONE, 'Output in json format')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$userId = $input->getOption('user_id');
$appId = $input->getOption('app_id');
$name = $input->getOption('name');
$type = $input->getOption('type');
$description = $input->getOption('description');
$website = $input->getOption('website');
$urlCallback = $input->getOption('callback');
$webhookUrl = $input->getOption('webhook_url');
$active = $input->getOption('active');
$generateToken = $input->getOption('generate_token');
$passwordOauth2Gt = $input->getOption('password_oauth2_gt');
$create = $input->getOption('create');
$edit = $input->getOption('edit');
$delete = $input->getOption('delete');
$list = $input->getOption('list');
$jsonformat = $input->getOption('jsonformat');
$applicationManipulator = $this->container['manipulator.api-application'];
$apiOauthTokenManipulator = $this->container['manipulator.api-oauth-token'];
$accountRepository = $this->container['repo.api-accounts'];
$apiApllicationConverter = $this->container['converter.api-application'];
$userRepository = $this->container['repo.users'];
$apiOauthRepository = $this->container['repo.api-oauth-tokens'];
if ($create) {
if (null === $user = $userRepository->find($userId)) {
$output->writeln('<error>User not found</error>');
return 0;
}
if (!$name) {
$output->writeln('<error>Name of application must be provide with option --name.</error>');
return 0;
}
if (!$description) {
$output->writeln('<error>Desciption of application must be provide.</error>');
return 0;
}
try {
$application = $applicationManipulator
->create(
$name,
$type,
$description,
$website,
$user,
$urlCallback
);
$apiAccountManipulator = $this->container['manipulator.api-account'];
$apiAccountManipulator->create($application, $user, V2::VERSION);
$account = $accountRepository->findByUserAndApplication($user, $application);
if ($generateToken) {
$apiOauthTokenManipulator->create($account);
}
if ($passwordOauth2Gt) {
if (in_array($passwordOauth2Gt, ['true', 'false'])) {
$application->setGrantPassword(($passwordOauth2Gt == 'true') ? true : false);
$applicationManipulator->update($application);
} else {
$output->writeln('<error> Value of option --password_oauth2_gt should be "true" or "false"</error>');
return 0;
}
}
if ($webhookUrl) {
$applicationManipulator->setWebhookUrl($application, $webhookUrl);
$applicationManipulator->update($application);
}
if ($active) {
if (in_array($active, ['true', 'false'])) {
$application->setActivated(($active == 'true') ? true : false);
$applicationManipulator->update($application);
} else {
$output->writeln('<error>Value of option --active should be "true" or "false"</error>');
return 0;
}
} else {
$application->setActivated(true);
$applicationManipulator->update($application);
}
$this->showApllicationInformation($apiOauthRepository, $account, $application, $jsonformat, $output);
} catch (\Exception $e) {
$output->writeln('<error>Create an application for user failed : '.$e->getMessage().'</error>');
}
} elseif ($edit) {
if (!$appId) {
$output->writeln('<error>ID of the application must be provided with option --app_id to edit the application.</error>');
return 0;
}
$application = $apiApllicationConverter->convert($appId);
$account = $accountRepository->findByUserAndApplication($application->getCreator(), $application);
if (!$account) {
$output->writeln('<error>ApiAccount not found!</error>');
return 0;
}
if ($name) {
$application->setName($name);
}
if ($type) {
$applicationManipulator->setType($application, $type);
if ($type == ApiApplication::DESKTOP_TYPE) {
$applicationManipulator->setRedirectUri($application, ApiApplication::NATIVE_APP_REDIRECT_URI);
}
}
if ($description) {
$application->setDescription($description);
}
if ($website) {
$applicationManipulator->setWebsiteUrl($application, $website);
}
if ($urlCallback) {
$applicationManipulator->setRedirectUri($application, $urlCallback);
}
if ($generateToken) {
if (null !== $devToken = $apiOauthRepository->findDeveloperToken($account)) {
$apiOauthTokenManipulator->renew($devToken);
} else {
$apiOauthTokenManipulator->create($account);
}
}
if ($passwordOauth2Gt) {
if (in_array($passwordOauth2Gt, ['true', 'false'])) {
$application->setGrantPassword(($passwordOauth2Gt == 'true') ? true : false);
} else {
$output->writeln('<error> Value of option --password_oauth2_gt should be "true" or "false"</error>');
return 0;
}
}
if ($webhookUrl) {
$applicationManipulator->setWebhookUrl($application, $webhookUrl);
}
if ($active) {
if (in_array($active, ['true', 'false'])) {
$application->setActivated(($active == 'true') ? true : false);
} else {
$output->writeln('<error>Value of option --active should be "true" or "false"</error>');
return 0;
}
}
$applicationManipulator->update($application);
$this->showApllicationInformation($apiOauthRepository, $account, $application, $jsonformat, $output);
} elseif ($list) {
if ($userId) {
if (null === $user = $userRepository->find($userId)) {
$output->writeln('<error>User not found</error>');
return 0;
}
$accounts = $accountRepository->findByUser($user);
} else {
$accounts = $accountRepository->findAll();
}
$applicationList = [];
foreach ($accounts as $account) {
$application = $account->getApplication();
$token = $apiOauthRepository->findDeveloperToken($account);
$applicationList[] = [
$application->getId(),
$account->getUser()->getId(),
$application->getName(),
$application->getClientId(),
$application->getClientSecret(),
$application->getRedirectUri(),
($token) ? $token->getOauthToken() : '-',
$application->isPasswordGranted() ? "true": "false"
];
}
$applicationTable = $this->getHelperSet()->get('table');
$headers = ['app_id', 'user_id', 'name', 'client_id', 'client_secret', 'callback_url', 'generated token', 'grant_password status'];
if ($jsonformat ) {
foreach ($applicationList as $appList) {
$appInfo[] = array_combine($headers, $appList);
}
echo json_encode($appInfo);
} else {
$applicationTable = $this->getHelperSet()->get('table');
$applicationTable
->setHeaders($headers)
->setRows($applicationList)
->render($output)
;
}
} elseif ($delete) {
if (!$appId) {
$output->writeln('<error>ID of the application must be provided with option --app_id to delete the app.</error>');
return 0;
}
$application = $apiApllicationConverter->convert($appId);
if (is_null($application->getCreator())) {
/** @var DialogHelper $dialog */
$dialog = $this->getHelperSet()->get('dialog');
$continue = $dialog->askConfirmation($output, "<question>It's a special phraseanet application, do you want really to delete it? (N/y)</>", false);
if (!$continue) {
$output->writeln("<info>See you later !</>");
return 0;
}
}
$applicationManipulator->delete($application);
$output->writeln("<info>Application ID $appId deleted successfully !</info>");
}
return 0;
}
private function showApllicationInformation($apiOauthRepository, ApiAccount $account, ApiApplication $application, $jsonformat, $output)
{
$token = $account ? $apiOauthRepository->findDeveloperToken($account) : null;
$applicationCreated = [
$application->getClientSecret(),
$application->getClientId(),
$this->container["conf"]->get("servername") . "api/oauthv2/authorize",
$this->container["conf"]->get("servername") . "api/oauthv2/token",
($token) ? $token->getOauthToken() : '-',
$application->isPasswordGranted() ? "true": "false"
];
$headers = ['client_secret', 'client_id', 'Authorize endpoint url', 'Access endpoint', 'generated token', 'grant_password status'];
if ($jsonformat ) {
$createdAppInfo = array_combine($headers, $applicationCreated);
echo json_encode($createdAppInfo);
} else {
$table = $this->getHelperSet()->get('table');
$table
->setHeaders($headers)
->setRows([$applicationCreated])
->render($output)
;
}
}
}

View File

@@ -0,0 +1,211 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\User;
use Alchemy\Phrasea\Application\Helper\NotifierAware;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Notification\Mail\MailRequestPasswordSetup;
use Alchemy\Phrasea\Notification\Mail\MailRequestEmailConfirmation;
use Alchemy\Phrasea\Notification\Receiver;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class UserCreateCommand extends Command
{
use NotifierAware;
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('user:create');
$this->setDescription('Create user in Phraseanet')
->addOption('user_login', null, InputOption::VALUE_REQUIRED, 'The desired login for created user.')
->addOption('user_mail', null, InputOption::VALUE_OPTIONAL, 'The desired mail for created user.')
->addOption('user_password', null, InputOption::VALUE_OPTIONAL, 'The desired password')
->addOption('send_mail_confirm', null, InputOption::VALUE_NONE, 'Send an email to user, for validate email.')
->addOption('send_mail_password', null, InputOption::VALUE_NONE, 'Send an email to user, for password definition, work only if user_password is not define')
->addOption('model_number', null, InputOption::VALUE_OPTIONAL, 'Id of model')
->addOption('user_gender', null, InputOption::VALUE_OPTIONAL, 'The gender for created user.')
->addOption('user_firstname', null, InputOption::VALUE_OPTIONAL, 'The first name for created user.')
->addOption('user_lastname', null, InputOption::VALUE_OPTIONAL, 'The last name for created user.')
->addOption('user_compagny', null, InputOption::VALUE_OPTIONAL, 'The compagny for created user.')
->addOption('user_job', null, InputOption::VALUE_OPTIONAL, 'The job for created user.')
->addOption('user_activitie', null, InputOption::VALUE_OPTIONAL, 'The activitie for created user.')
->addOption('user_phone', null, InputOption::VALUE_OPTIONAL, 'The phone number for created user.')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$userLogin = $input->getOption('user_login');
$userMail = $input->getOption('user_mail');
$userPassword = $input->getOption('user_password');
$sendMailConfirm = $input->getOption('send_mail_confirm');
$sendMailPassword = $input->getOption('send_mail_password');
$modelNumber = $input->getOption('model_number');
$userGender = $input->getOption('user_gender');
$userFirstName = $input->getOption('user_firstname');
$userLastName = $input->getOption('user_lastname');
$userCompagny = $input->getOption('user_compagny');
$userJob = $input->getOption('user_job');
$userActivity = $input->getOption('user_activitie');
$userPhone = $input->getOption('user_phone');
$userRepository = $this->container['repo.users'];
if ($userMail) {
if (!\Swift_Validate::email($userMail)) {
$output->writeln('<error>Invalid mail address</error>');
return 0;
}
if (null !== $userRepository->findByEmail($userMail)) {
$output->writeln('<error>An user exist with this email.</error>');
return 0;
}
}
$password = (!is_null($userPassword)) ? $userPassword : $this->container['random.medium']->generateString(128);
$userManipulator = $this->container['manipulator.user'];
$user = $userManipulator->createUser($userLogin, $password, $userMail);
if ($userGender) {
if (null === $gender = $this->verifyGender($userGender)) {
$output->writeln('<bg=yellow;options=bold>Gender '.$userGender.' not exists.</>');
}
$user->setGender($gender);
}
if($userFirstName) $user->setFirstName($userFirstName);
if($userLastName) $user->setLastName($userLastName);
if($userCompagny) $user->setCompany($userCompagny);
if($userJob) $user->setJob($userJob);
if($userActivity) $user->setActivity($userActivity);
if($userPhone) $user->setPhone($userPhone);
if ($sendMailPassword and $userMail and is_null($userPassword)) {
$this->sendPasswordSetupMail($user);
}
if ($sendMailConfirm and $userMail) {
$user->setMailLocked(true);
$this->sendAccountUnlockEmail($user);
}
if ($modelNumber) {
$template = $userRepository->find($modelNumber);
if (!$template) {
$output->writeln('<bg=yellow;options=bold>Model '.$modelNumber.' not found.</>');
} else {
$base_ids = [];
foreach ($this->container->getApplicationBox()->get_databoxes() as $databox) {
foreach ($databox->get_collections() as $collection) {
$base_ids[] = $collection->get_base_id();
}
}
$this->container->getAclForUser($user)->apply_model($template, $base_ids);
}
}
$this->container['orm.em']->flush();
$output->writeln("<info>Create new user successful !</info>");
return 0;
}
/**
* Get gender for user
* @param $type
* @return int|null
*/
private function verifyGender($type)
{
switch (strtolower($type)) {
case "mlle.":
case "mlle":
case "miss":
case "mademoiselle":
case "0":
$gender = User::GENDER_MISS;
break;
case "mme":
case "madame":
case "ms":
case "ms.":
case "1":
$gender = User::GENDER_MRS;
break;
case "m":
case "m.":
case "mr":
case "mr.":
case "monsieur":
case "mister":
case "2":
$gender = User::GENDER_MR;
break;
default:
$gender = null;
}
return $gender;
}
/**
* Send mail for renew password
* @param User $user
*/
public function sendPasswordSetupMail(User $user)
{
$this->setDelivererLocator(new LazyLocator($this->container, 'notification.deliverer'));
$receiver = Receiver::fromUser($user);
$token = $this->container['manipulator.token']->createResetPasswordToken($user);
$mail = MailRequestPasswordSetup::create($this->container, $receiver);
$servername = $this->container['conf']->get('servername');
$mail->setButtonUrl('http://'.$servername.'/login/renew-password/?token='.$token->getValue());
$mail->setLogin($user->getLogin());
$this->deliver($mail);
}
/**
* @param User $user
*/
public function sendAccountUnlockEmail(User $user)
{
$this->setDelivererLocator(new LazyLocator($this->container, 'notification.deliverer'));
$receiver = Receiver::fromUser($user);
$token = $this->container['manipulator.token']->createAccountUnlockToken($user);
$mail = MailRequestEmailConfirmation::create($this->container, $receiver);
$servername = $this->container['conf']->get('servername');
$mail->setButtonUrl('http://'.$servername.'/login/register-confirm/?code='.$token->getValue());
$mail->setExpiration($token->getExpiration());
$this->deliver($mail);
}
}

View File

@@ -0,0 +1,236 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\User;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Model\Entities\User;
use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Alchemy\Phrasea\Utilities\NullableDateTime;
class UserListCommand extends Command
{
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('user:list');
$this->setDescription('List of all user <comment>(experimental)</>')
->addOption('user_id', null, InputOption::VALUE_OPTIONAL, 'The id of user export only info this user ')
->addOption('user_email', null, InputOption::VALUE_OPTIONAL, 'The mail of user export only info this user .')
->addOption('database_id', null, InputOption::VALUE_OPTIONAL, 'Id of database.')
->addOption('collection_id', null, InputOption::VALUE_OPTIONAL, 'Id of the collection.')
->addOption('mail_lock_status', null, InputOption::VALUE_NONE, 'Status by mail locked')
->addOption('guest', null, InputOption::VALUE_NONE, 'Only guest user')
->addOption('created', null, InputOption::VALUE_OPTIONAL, 'Created at with operator,aaaa-mm-jj hh:mm:ss.')
->addOption('updated', null, InputOption::VALUE_OPTIONAL, 'Update at with operator,aaaa-mm-jj hh:mm:ss.')
->addOption('right', null, InputOption::VALUE_NONE, 'Show right information')
->addOption('adress', null, InputOption::VALUE_NONE, 'Show adress information')
->addOption('models', null, InputOption::VALUE_NONE, "Show only defined models, if --user_id is set with --models it's the template owner")
->addOption('jsonformat', null, InputOption::VALUE_NONE, 'Output in json format')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$userId = $input->getOption('user_id');
$userEmail = $input->getOption('user_email');
$databaseId = $input->getOption('database_id');
$collectionId = $input->getOption('collection_id');
$lockStatus = $input->getOption('mail_lock_status');
$guest = $input->getOption('guest');
$withAdress = $input->getOption('adress');
$created = $input->getOption('created');
$updated = $input->getOption('updated');
$withRight = $input->getOption('right');
$models = $input->getOption('models');
$jsonformat = $input->getOption('jsonformat');
$query = $this->container['phraseanet.user-query'];
if($databaseId) $query->on_base_ids([$databaseId]);
if($collectionId) $query->on_sbas_ids([$collectionId]);
if($created) $this->addFilterDate($created,'created',$query);
if($updated) $this->addFilterDate($updated,'updated',$query);
if($userId && !$models) $query->addSqlFilter('Users.id = ?' ,[$userId]);
if($userEmail && !$models) $query->addSqlFilter('Users.email = ?' ,[$userEmail]);
if($lockStatus && !$models) $query->addSqlFilter('Users.mail_locked = 1');
if($guest && !$models) $query->include_invite(true)->addSqlFilter('Users.guest = 1');
/** @var UserRepository $userRepository */
$userRepository = $this->container['repo.users'];
if ($models && $userId) {
$users = $userRepository->findBy(['templateOwner' => $userId]);
} elseif ($models) {
$users = $userRepository->findTemplate();
} else {
$users = $query->execute()->get_results();
}
$userList = [];
foreach ($users as $key => $user) {
$userList[] = $this->listUser($user, $withAdress, $withRight);
$userListRaw[] = array_combine($this->headerTable($withAdress, $withRight), $this->listUser($user, $withAdress, $withRight));
}
if ($jsonformat) {
echo json_encode($userListRaw);
} else {
$table = $this->getHelperSet()->get('table');
$table
->setHeaders($this->headerTable($withAdress, $withRight))
->setRows($userList)
->render($output);
;
}
return 0;
}
/**
* @param $withAdress
* @param $withRight
* @return array
*/
private function headerTable($withAdress,$withRight)
{
$defaultHeader = ['id', 'login', 'email','last_model','first_name','last_name','gender','created','updated','status','locale'];
$adressHeader = [ 'address', 'zip_code', 'city', 'country', 'phone', 'fax', 'job','position', 'company', 'geoname_id'];
$rightHeader = [ 'admin', 'guest', 'mail_notification', 'ldap_created', 'mail_locked'];
return $this->createInformation($withAdress,$withRight,$defaultHeader,['adress' => $adressHeader,'right' =>$rightHeader]);
}
/**
* @param User $user
* @param $withAdress
* @param $withRight
* @return array
*/
private function listUser(User $user,$withAdress,$withRight)
{
switch ($user->getGender()) {
case User::GENDER_MRS:
$gender = 'Mrs';
break;
case User::GENDER_MISS:
$gender = 'Miss';
break;
case User::GENDER_MR:
default:
$gender = 'Mr';
}
$defaultInfo = [
$user->getId(),
$user->getLogin() ?: '-',
$user->getEmail() ?: '-',
$user->getLastAppliedTemplate() ? $user->getLastAppliedTemplate()->getLogin() : '-',
$user->getFirstName() ?: '-',
$user->getLastName() ?: '-',
$gender,
NullableDateTime::format($user->getCreated(),'Y-m-d H:i:s'),
NullableDateTime::format($user->getUpdated(),'Y-m-d H:i:s'),
'status',
$user->getLocale() ?: '-',
];
return $this->createInformation($withAdress,$withRight,$defaultInfo,['adress' => $this->userAdress($user),'right' => $this->userRight($user)]);
}
/**
* @param User $user
* @return array
*/
private function userAdress(User $user)
{
return [
$user->getAddress() ?: '-',
$user->getZipCode() ?: '-',
$user->getCity() ?: '-',
$user->getCountry() ?: '-',
$user->getPhone() ?: '-',
$user->getFax() ?: '-',
$user->getJob() ?: '-',
$user->getActivity() ?: '-',
$user->getCompany() ?: '-',
$user->getGeonameId() ?: '-',
];
}
/**
* @param User $user
* @return array
*/
private function userRight(User $user)
{
return [
$user->isAdmin() ?: false,
$user->isGuest() ?: false,
$user->hasMailNotificationsActivated() ?: false,
$user->hasLdapCreated() ?: false,
$user->isMailLocked() ?: false,
];
}
/**
* @param $withAdress
* @param $withRight
* @param $default
* @param $infoToMerge
* @return array
*/
private function createInformation($withAdress,$withRight,$default,$infoToMerge)
{
if ($withAdress && $withRight) {
$information = array_merge($default, $infoToMerge['adress'],$infoToMerge['right']);
} elseif ($withAdress && !$withRight) {
$information = array_merge($default, $infoToMerge['adress']);
} elseif(!$withAdress && $withRight) {
$information = array_merge($default, $infoToMerge['right']);
} else {
$information = $default;
}
return $information;
}
/**
* @param $date
* @param $type
* @param $query
*/
private function addFilterDate($date,$type,$query){
list($operator,$dateAt) = explode(',', $date);
if (!in_array($operator,['=','>=','<=','>','<'])) {
throw new \InvalidArgumentException(" '=' or '<=' or '>=' or '>' or '<'");
}
$query->addSqlFilter($type.$operator.' ?' ,[$dateAt]);
}
}

View File

@@ -0,0 +1,178 @@
<?php
/*
* This file is part of Phraseanet
*
* (c) 2005-2016 Alchemy
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Command\User;
use Alchemy\Phrasea\Application\Helper\NotifierAware;
use Alchemy\Phrasea\Command\Command;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Notification\Receiver;
use Alchemy\Phrasea\Notification\Mail\MailRequestPasswordUpdate;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class UserPasswordCommand extends Command
{
use NotifierAware;
/**
* Constructor
*/
public function __construct($name = null)
{
parent::__construct('user:password');
$this->setDescription('Set user password in Phraseanet <comment>(experimental)</>')
->addOption('user_id', null, InputOption::VALUE_REQUIRED, 'The id of user.')
->addOption('generate', null, InputOption::VALUE_NONE, 'Generate and set with a random value')
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Set the user password to the input value')
->addOption('send_renewal_email', null, InputOption::VALUE_NONE, 'Send email link to user for password renewing, work only if --password or --generate are not define')
->addOption('password_hash', null, InputOption::VALUE_OPTIONAL, 'Define a password hashed, work only with password_nonce')
->addOption('password_nonce', null, InputOption::VALUE_OPTIONAL, 'Define a password nonce, work only with password_hash')
->addOption('dump', null, InputOption::VALUE_NONE, 'Return the password hashed and nonce')
->addOption('jsonformat', null, InputOption::VALUE_NONE, 'Output in json format')
->addOption('yes', 'y', InputOption::VALUE_NONE, 'Answer yes to all questions')
->setHelp('');
return $this;
}
protected function doExecute(InputInterface $input, OutputInterface $output)
{
$dialog = $this->getHelperSet()->get('dialog');
$userRepository = $this->container['repo.users'];
$userManipulator = $this->container['manipulator.user'];
$user = $userRepository->find($input->getOption('user_id'));
$password = $input->getOption('password');
$generate = $input->getOption('generate');
$sendRenewalEmail = $input->getOption('send_renewal_email');
$dump = $input->getOption('dump');
$passwordHash = $input->getOption('password_hash');
$passwordNonce = $input->getOption('password_nonce');
$jsonformat = $input->getOption('jsonformat');
$yes = $input->getOption('yes');
if ($user === null) {
$output->writeln('<info>Not found User.</info>');
return 0;
}
if ($passwordHash && $passwordNonce) {
$user->setNonce($passwordNonce);
$user->setPassword($passwordHash);
$userManipulator->updateUser($user);
$output->writeln('<info>password set with hashed pass</info>');
return 0;
}
if ($dump) {
$oldHash = $user->getPassword();
$oldNonce = $user->getNonce();
}
if ($generate) {
$oldHash = $user->getPassword();
$oldNonce = $user->getNonce();
$password = $this->container['random.medium']->generateString(64);
} else {
if (!$password && $sendRenewalEmail) {
$this->sendPasswordSetupMail($user);
$output->writeln('<info>email link sended for password renewing!</info>');
return 0;
} elseif (!$password && !$sendRenewalEmail && ! $dump) {
$output->writeln('<error>choose one option to set a password!</error>');
return 0;
}
}
if ($password) {
if (!$yes) {
do {
$continue = mb_strtolower($dialog->ask($output, '<question>Do you want really set password to this user? (y/N)</question>', 'N'));
} while (!in_array($continue, ['y', 'n']));
if ($continue !== 'y') {
$output->writeln('Aborting !');
return;
}
}
$oldHash = $user->getPassword();
$oldNonce = $user->getNonce();
$userManipulator->setPassword($user,$password);
}
if ($dump) {
if ($jsonformat) {
$hash['password_hash'] = $oldHash;
$hash['nonce'] = $oldNonce;
echo json_encode($hash);
return 0;
} else {
$output->writeln('<info>password_hash :</info>' . $oldHash);
$output->writeln('<info>nonce :</info>' . $oldNonce);
return 0;
}
}
if (($password || $generate)) {
if ($jsonformat) {
$hash['new_password'] = $password;
$hash['previous_password_hash'] = $oldHash;
$hash['previous_nonce'] = $oldNonce;
echo json_encode($hash);
} else {
$output->writeln('<info>new_password :</info>' . $password);
$output->writeln('<info>previous_password_hash :</info>' . $oldHash);
$output->writeln('<info>previous_nonce :</info>' . $oldNonce);
}
}
return 0;
}
/**
* Send mail for renew password
* @param User $user
*/
private function sendPasswordSetupMail(User $user)
{
$this->setDelivererLocator(new LazyLocator($this->container, 'notification.deliverer'));
$receiver = Receiver::fromUser($user);
$token = $this->container['manipulator.token']->createResetPasswordToken($user);
$url = $this->container['url_generator']->generate('login_renew_password', [ 'token' => $token->getValue() ], true);
$mail = MailRequestPasswordUpdate::create($this->container, $receiver);
$servername = $this->container['conf']->get('servername');
$mail->setButtonUrl($url);
$mail->setLogin($user->getLogin());
$mail->setExpiration(new \DateTime('+1 day'));
$this->deliver($mail);
}
}

View File

@@ -314,6 +314,9 @@ class FieldsController extends Controller
->set_readonly($data['readonly'])
->set_type($data['type'])
->set_tbranch($data['tbranch'])
->set_generate_cterms($data['generate_cterms'])
->set_gui_editable($data['gui_editable'])
->set_gui_visible($data['gui_visible'])
->set_report($data['report'])
->setVocabularyControl(null)
->setVocabularyRestricted(false);
@@ -349,7 +352,7 @@ class FieldsController extends Controller
{
return [
'name', 'multi', 'thumbtitle', 'tag', 'business', 'indexable', 'aggregable',
'required', 'separator', 'readonly', 'type', 'tbranch', 'report',
'required', 'separator', 'readonly', 'gui_editable', 'gui_visible' , 'type', 'tbranch', 'generate_cterms', 'report',
'vocabulary-type', 'vocabulary-restricted', 'dces-element', 'labels'
];
}

View File

@@ -13,9 +13,11 @@ namespace Alchemy\Phrasea\Controller\Admin;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchSettingsFormType;
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchOptions;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\GlobalStructure;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use databox_descriptionStructure;
class SearchEngineController extends Controller
{
@@ -31,7 +33,19 @@ class SearchEngineController extends Controller
$form->handleRequest($request);
if ($form->isValid()) {
$this->saveElasticSearchOptions($form->getData());
/** @var ElasticsearchOptions $data */
$data = $form->getData();
// $q = $request->request->get('elasticsearch_settings');
$facetNames = []; // rebuild the data "_customValues/facets" list following the form order
foreach($request->request->get('elasticsearch_settings') as $name=>$value) {
$matches = null;
if(preg_match('/^facets:(.+):limit$/', $name, $matches) === 1) {
$facetNames[] = $matches[1];
}
}
$data->reorderAggregableFields($facetNames);
$this->saveElasticSearchOptions($data);
return $this->app->redirectPath('admin_searchengine_form');
}
@@ -76,6 +90,16 @@ class SearchEngineController extends Controller
*/
private function saveElasticSearchOptions(ElasticsearchOptions $configuration)
{
// save to databoxes fields for backward compatibility (useless ?)
foreach($configuration->getAggregableFields() as $fname=>$aggregableField) {
foreach ($this->app->getDataboxes() as $databox) {
if(!is_null($f = $databox->get_meta_structure()->get_element_by_name($fname, databox_descriptionStructure::STRICT_COMPARE))) {
$f->set_aggregable($aggregableField['limit'])->save();
}
}
}
// save to conf
$this->getConf()->set(['main', 'search-engine', 'options'], $configuration->toArray());
}
@@ -85,7 +109,10 @@ class SearchEngineController extends Controller
*/
private function getConfigurationForm(ElasticsearchOptions $options)
{
return $this->app->form(new ElasticsearchSettingsFormType(), $options, [
/** @var GlobalStructure $g */
$g = $this->app['search_engine.structure'];
return $this->app->form(new ElasticsearchSettingsFormType($g, $options), $options, [
'action' => $this->app->url('admin_searchengine_form'),
]);
}

View File

@@ -354,7 +354,7 @@ class SubdefsController extends Controller
Subdef::TYPE_VIDEO => [
"definitions" => [
"video codec H264" => null,
"144P H264 128 kbps ACC 128kbps" => [
"144P H264 128 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "128",
@@ -362,10 +362,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "256",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"240P H264 256 kbps ACC 128kbps" => [
"240P H264 256 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "256",
@@ -373,21 +373,21 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "426",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"360P H264 576 kbps ACC 128kbps" => [
"360P H264 576 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "576",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "480",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libtheora",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"480P H264 750 kbps ACC 128kbps" => [
"480P H264 750 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "750",
@@ -395,10 +395,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "854",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"720P H264 1492 kbps ACC 128kbps" => [
"720P H264 1492 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "1492",
@@ -406,10 +406,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "1280",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"1080P H264 2420 kbps ACC 128kbps" => [
"1080P H264 2420 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "2420",
@@ -417,11 +417,77 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "1920",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"144P H264 128 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "128",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "256",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"240P H264 256 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "256",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "426",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"360P H264 576 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "576",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "480",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"480P H264 750 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "750",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "854",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"720P H264 1492 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "1492",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "1280",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"1080P H264 2420 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "2420",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "1920",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libx264",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"video codec libvpx" => null,
"144P webm 128 kbps ACC 128kbps" => [
"144P webm 128 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "128",
@@ -429,10 +495,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "256",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"240P webm 256 kbps ACC 128kbps" => [
"240P webm 256 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "256",
@@ -440,10 +506,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "426",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"360P webm 576 kbps ACC 128kbps" => [
"360P webm 576 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "576",
@@ -451,10 +517,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "480",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"480P webm 750 kbps ACC 128kbps" => [
"480P webm 750 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "750",
@@ -462,10 +528,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "854",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"720P webm 1492 kbps ACC 128kbps" => [
"720P webm 1492 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "1492",
@@ -473,10 +539,10 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "1280",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"1080P webm 2420 kbps ACC 128kbps" => [
"1080P webm 2420 kbps MP3 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "2420",
@@ -484,7 +550,73 @@ class SubdefsController extends Controller
Video::OPTION_SIZE => "1920",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfaac",
Video::OPTION_ACODEC => "libmp3lame",
Subdef::OPTION_DEVICE => ["all"]
],
"144P webm 128 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "128",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "256",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"240P webm 256 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "256",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "426",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"360P webm 576 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "576",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "480",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"480P webm 750 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "750",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "854",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"720P webm 1492 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "1492",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "1280",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
"1080P webm 2420 kbps AAC 128kbps" => [
Video::OPTION_AUDIOBITRATE => "128",
Video::OPTION_AUDIOSAMPLERATE => "44100",
Video::OPTION_BITRATE => "2420",
Video::OPTION_GOPSIZE => "25",
Video::OPTION_SIZE => "1920",
Video::OPTION_FRAMERATE => "25",
Video::OPTION_VCODEC => "libvpx",
Video::OPTION_ACODEC => "libfdk_aac",
Subdef::OPTION_DEVICE => ["all"]
],
],

View File

@@ -515,9 +515,9 @@ class UserController extends Controller
$denyColl[] = $label;
$hookData['rejected'][$bas] = $label;
}
}
$this->app['manipulator.webhook-event']->create($hookName, $hookType, $hookData);
$this->app['manipulator.webhook-event']->create($hookName, $hookType, $hookData, [$bas]);
}
if ($user->hasMailNotificationsActivated() && (0 !== count($acceptColl) || 0 !== count($denyColl))) {
$message = '';

View File

@@ -12,6 +12,7 @@
namespace Alchemy\Phrasea\Controller\Api;
use Alchemy\Phrasea\ControllerProvider\Api\V1;
use Alchemy\Phrasea\ControllerProvider\Api\V3;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -272,6 +273,8 @@ class Result
$this->version = $this->request->attributes->get('api_version');
} elseif (mb_strpos($this->request->getPathInfo(), '/api/v1') !== FALSE) {
$this->version = V1::VERSION;
} elseif (mb_strpos($this->request->getPathInfo(), '/api/v3') !== FALSE) {
$this->version = V3::VERSION;
} else {
$this->version = self::$defaultVersion;
}

View File

@@ -594,6 +594,9 @@ class V1Controller extends Controller
],
'separator' => $databox_field->get_separator(),
'thesaurus_branch' => $databox_field->get_tbranch(),
'generate_cterms' => $databox_field->get_generate_cterms(),
'gui_editable' => $databox_field->get_gui_editable(),
'gui_visible' => $databox_field->get_gui_visible(),
'type' => $databox_field->get_type(),
'indexable' => $databox_field->is_indexable(),
'multivalue' => $databox_field->is_multi(),
@@ -1570,9 +1573,9 @@ class V1Controller extends Controller
$options->setFirstResult((int)($request->get('offset_start') ?: 0));
$options->setMaxResults((int)$request->get('per_page') ?: 10);
$this->getSearchEngine()->resetCache();
$searchEngine = $this->getSearchEngine();
$search_result = $this->getSearchEngine()->query((string)$request->get('query'), $options);
$search_result = $searchEngine->query((string)$request->get('query'), $options);
$this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQueryText());
@@ -1580,12 +1583,12 @@ class V1Controller extends Controller
$collectionsReferencesByDatabox = $options->getCollectionsReferencesByDatabox();
foreach ($collectionsReferencesByDatabox as $sbid => $references) {
$databox = $this->findDataboxById($sbid);
$collectionsIds = array_map(function(CollectionReference $ref){return $ref->getCollectionId();}, $references);
$collectionsIds = array_map(function (CollectionReference $ref) {
return $ref->getCollectionId();
}, $references);
$this->getSearchEngineLogger()->log($databox, $search_result->getQueryText(), $search_result->getTotal(), $collectionsIds);
}
$this->getSearchEngine()->clearCache();
return $search_result;
}

View File

@@ -0,0 +1,807 @@
<?php
namespace Alchemy\Phrasea\Controller\Api;
use Alchemy\Phrasea\Collection\Reference\CollectionReference;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Databox\DataboxGroupable;
use Alchemy\Phrasea\Fractal\CallbackTransformer;
use Alchemy\Phrasea\Fractal\IncludeResolver;
use Alchemy\Phrasea\Fractal\SearchResultTransformerResolver;
use Alchemy\Phrasea\Fractal\TraceableArraySerializer;
use Alchemy\Phrasea\Model\Manipulator\UserManipulator;
use Alchemy\Phrasea\Model\RecordReferenceInterface;
use Alchemy\Phrasea\Record\RecordCollection;
use Alchemy\Phrasea\Record\RecordReferenceCollection;
use Alchemy\Phrasea\Search\CaptionView;
use Alchemy\Phrasea\Search\PermalinkTransformer;
use Alchemy\Phrasea\Search\PermalinkView;
use Alchemy\Phrasea\Search\RecordTransformer;
use Alchemy\Phrasea\Search\RecordView;
use Alchemy\Phrasea\Search\SearchResultView;
use Alchemy\Phrasea\Search\StoryTransformer;
use Alchemy\Phrasea\Search\StoryView;
use Alchemy\Phrasea\Search\SubdefTransformer;
use Alchemy\Phrasea\Search\SubdefView;
use Alchemy\Phrasea\Search\TechnicalDataTransformer;
use Alchemy\Phrasea\Search\TechnicalDataView;
use Alchemy\Phrasea\Search\V1SearchCompositeResultTransformer;
use Alchemy\Phrasea\Search\V1SearchResultTransformer;
use Alchemy\Phrasea\SearchEngine\SearchEngineInterface;
use Alchemy\Phrasea\SearchEngine\SearchEngineLogger;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
use Alchemy\Phrasea\SearchEngine\SearchEngineResult;
use League\Fractal\Resource\Item;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class V3Controller extends Controller
{
/**
* Return detailed information about one story
*
* @param Request $request
* @param int $databox_id
* @param int $record_id
*
* @return Response
*/
public function getStoryAction(Request $request, $databox_id, $record_id)
{
try {
$story = $this->findDataboxById($databox_id)->get_record($record_id);
return Result::create($request, ['story' => $this->listStory($request, $story)])->createResponse();
} catch (NotFoundHttpException $e) {
return Result::createError($request, 404, $this->app->trans('Story Not Found'))->createResponse();
} catch (\Exception $e) {
return $this->app['controller.api.v1']->getBadRequestAction($request, $this->app->trans('An error occurred'));
}
}
/**
* Search for results
*
* @param Request $request
*
* @return Response
*/
public function searchAction(Request $request)
{
$subdefTransformer = new SubdefTransformer($this->app['acl'], $this->getAuthenticatedUser(), new PermalinkTransformer());
$technicalDataTransformer = new TechnicalDataTransformer();
$recordTransformer = new RecordTransformer($subdefTransformer, $technicalDataTransformer);
$storyTransformer = new StoryTransformer($subdefTransformer, $recordTransformer);
$compositeTransformer = new V1SearchCompositeResultTransformer($recordTransformer, $storyTransformer);
$searchTransformer = new V1SearchResultTransformer($compositeTransformer);
$transformerResolver = new SearchResultTransformerResolver([
'' => $searchTransformer,
'results' => $compositeTransformer,
'results.stories' => $storyTransformer,
'results.stories.thumbnail' => $subdefTransformer,
'results.stories.metadatas' => new CallbackTransformer(),
'results.stories.caption' => new CallbackTransformer(),
'results.stories.records' => $recordTransformer,
'results.stories.records.thumbnail' => $subdefTransformer,
'results.stories.records.technical_informations' => $technicalDataTransformer,
'results.stories.records.subdefs' => $subdefTransformer,
'results.stories.records.metadata' => new CallbackTransformer(),
'results.stories.records.status' => new CallbackTransformer(),
'results.stories.records.caption' => new CallbackTransformer(),
'results.records' => $recordTransformer,
'results.records.thumbnail' => $subdefTransformer,
'results.records.technical_informations' => $technicalDataTransformer,
'results.records.subdefs' => $subdefTransformer,
'results.records.metadata' => new CallbackTransformer(),
'results.records.status' => new CallbackTransformer(),
'results.records.caption' => new CallbackTransformer(),
]);
$includeResolver = new IncludeResolver($transformerResolver);
$fractal = new \League\Fractal\Manager();
$fractal->setSerializer(new TraceableArraySerializer($this->app['dispatcher']));
$fractal->parseIncludes($this->resolveSearchIncludes($request));
$result = $this->doSearch($request);
$story_max_records = null;
// if search on story
if ($request->get('search_type') == 1) {
$story_max_records = (int)$request->get('story_max_records') ?: 10;
}
$searchView = $this->buildSearchView(
$result,
$includeResolver->resolve($fractal),
$this->resolveSubdefUrlTTL($request),
$story_max_records
);
$ret = $fractal->createData(new Item($searchView, $searchTransformer))->toArray();
return Result::create($request, $ret)->createResponse();
}
/**
* Retrieve detailed information about one story
*
* @param Request $request
* @param \record_adapter $story
* @return array
* @throws \Exception
*/
private function listStory(Request $request, \record_adapter $story)
{
if (!$story->isStory()) {
return Result::createError($request, 404, 'Story not found')->createResponse();
}
$per_page = (int)$request->get('per_page')?:10;
$page = (int)$request->get('page')?:1;
$offset = ($per_page * ($page - 1)) + 1;
$caption = $story->get_caption();
$format = function (\caption_record $caption, $dcField) {
$field = $caption->get_dc_field($dcField);
if (!$field) {
return null;
}
return $field->get_serialized_values();
};
return [
'@entity@' => V1Controller::OBJECT_TYPE_STORY,
'databox_id' => $story->getDataboxId(),
'story_id' => $story->getRecordId(),
'updated_on' => $story->getUpdated()->format(DATE_ATOM),
'created_on' => $story->getCreated()->format(DATE_ATOM),
'collection_id' => $story->getCollectionId(),
'base_id' => $story->getBaseId(),
'thumbnail' => $this->listEmbeddableMedia($request, $story, $story->get_thumbnail()),
'uuid' => $story->getUuid(),
'metadatas' => [
'@entity@' => V1Controller::OBJECT_TYPE_STORY_METADATA_BAG,
'dc:contributor' => $format($caption, \databox_Field_DCESAbstract::Contributor),
'dc:coverage' => $format($caption, \databox_Field_DCESAbstract::Coverage),
'dc:creator' => $format($caption, \databox_Field_DCESAbstract::Creator),
'dc:date' => $format($caption, \databox_Field_DCESAbstract::Date),
'dc:description' => $format($caption, \databox_Field_DCESAbstract::Description),
'dc:format' => $format($caption, \databox_Field_DCESAbstract::Format),
'dc:identifier' => $format($caption, \databox_Field_DCESAbstract::Identifier),
'dc:language' => $format($caption, \databox_Field_DCESAbstract::Language),
'dc:publisher' => $format($caption, \databox_Field_DCESAbstract::Publisher),
'dc:relation' => $format($caption, \databox_Field_DCESAbstract::Relation),
'dc:rights' => $format($caption, \databox_Field_DCESAbstract::Rights),
'dc:source' => $format($caption, \databox_Field_DCESAbstract::Source),
'dc:subject' => $format($caption, \databox_Field_DCESAbstract::Subject),
'dc:title' => $format($caption, \databox_Field_DCESAbstract::Title),
'dc:type' => $format($caption, \databox_Field_DCESAbstract::Type),
],
'records' => $this->listRecords($request, array_values($story->getChildren($offset, $per_page)->get_elements())),
];
}
private function listEmbeddableMedia(Request $request, \record_adapter $record, \media_subdef $media)
{
if (!$media->is_physically_present()) {
return null;
}
if ($this->getAuthenticator()->isAuthenticated()) {
$acl = $this->getAclForUser();
if ($media->get_name() !== 'document'
&& false === $acl->has_access_to_subdef($record, $media->get_name())
) {
return null;
}
if ($media->get_name() === 'document'
&& !$acl->has_right_on_base($record->getBaseId(), \ACL::CANDWNLDHD)
&& !$acl->has_hd_grant($record)
) {
return null;
}
}
if ($media->get_permalink() instanceof \media_Permalink_Adapter) {
$permalink = $this->listPermalink($media->get_permalink());
} else {
$permalink = null;
}
$urlTTL = (int) $request->get(
'subdef_url_ttl',
$this->getConf()->get(['registry', 'general', 'default-subdef-url-ttl'])
);
if ($urlTTL < 0) {
$urlTTL = -1;
}
$issuer = $this->getAuthenticatedUser();
return [
'name' => $media->get_name(),
'permalink' => $permalink,
'height' => $media->get_height(),
'width' => $media->get_width(),
'filesize' => $media->get_size(),
'devices' => $media->getDevices(),
'player_type' => $media->get_type(),
'mime_type' => $media->get_mime(),
'substituted' => $media->is_substituted(),
'created_on' => $media->get_creation_date()->format(DATE_ATOM),
'updated_on' => $media->get_modification_date()->format(DATE_ATOM),
'url' => $this->app['media_accessor.subdef_url_generator']->generate($issuer, $media, $urlTTL),
'url_ttl' => $urlTTL,
];
}
private function listPermalink(\media_Permalink_Adapter $permalink)
{
$downloadUrl = $permalink->get_url();
$downloadUrl->getQuery()->set('download', '1');
return [
'created_on' => $permalink->get_created_on()->format(DATE_ATOM),
'id' => $permalink->get_id(),
'is_activated' => $permalink->get_is_activated(),
/** @Ignore */
'label' => $permalink->get_label(),
'updated_on' => $permalink->get_last_modified()->format(DATE_ATOM),
'page_url' => $permalink->get_page(),
'download_url' => (string)$downloadUrl,
'url' => (string)$permalink->get_url(),
];
}
/**
* @param Request $request
* @param RecordReferenceInterface[]|RecordReferenceCollection $records
* @return array
*/
private function listRecords(Request $request, $records)
{
if (!$records instanceof RecordReferenceCollection) {
$records = new RecordReferenceCollection($records);
}
$technicalData = $this->app['service.technical_data']->fetchRecordsTechnicalData($records);
$data = [];
foreach ($records->toRecords($this->getApplicationBox()) as $index => $record) {
$record->setTechnicalDataSet($technicalData[$index]);
$data[$index] = $this->listRecord($request, $record);
}
return $data;
}
/**
* Retrieve detailed information about one record
*
* @param Request $request
* @param \record_adapter $record
* @return array
*/
private function listRecord(Request $request, \record_adapter $record)
{
$technicalInformation = [];
foreach ($record->get_technical_infos()->getValues() as $name => $value) {
$technicalInformation[] = ['name' => $name, 'value' => $value];
}
$data = [
'databox_id' => $record->getDataboxId(),
'record_id' => $record->getRecordId(),
'mime_type' => $record->getMimeType(),
'title' => $record->get_title(),
'original_name' => $record->get_original_name(),
'updated_on' => $record->getUpdated()->format(DATE_ATOM),
'created_on' => $record->getCreated()->format(DATE_ATOM),
'collection_id' => $record->getCollectionId(),
'base_id' => $record->getBaseId(),
'sha256' => $record->getSha256(),
'thumbnail' => $this->listEmbeddableMedia($request, $record, $record->get_thumbnail()),
'technical_informations' => $technicalInformation,
'phrasea_type' => $record->getType(),
'uuid' => $record->getUuid(),
];
if ($request->attributes->get('_extended', false)) {
$data = array_merge($data, [
'subdefs' => $this->listRecordEmbeddableMedias($request, $record),
'metadata' => $this->listRecordMetadata($record),
'status' => $this->listRecordStatus($record),
'caption' => $this->listRecordCaption($record),
]);
}
return $data;
}
/**
* @param Request $request
* @param \record_adapter $record
* @return array
*/
private function listRecordEmbeddableMedias(Request $request, \record_adapter $record)
{
$subdefs = [];
foreach ($record->get_embedable_medias([], []) as $name => $media) {
if (null !== $subdef = $this->listEmbeddableMedia($request, $record, $media)) {
$subdefs[] = $subdef;
}
}
return $subdefs;
}
/**
* List all fields of given record
*
* @param \record_adapter $record
* @return array
*/
private function listRecordMetadata(\record_adapter $record)
{
$includeBusiness = $this->getAclForUser()->can_see_business_fields($record->getDatabox());
return $this->listRecordCaptionFields($record->get_caption()->get_fields(null, $includeBusiness));
}
/**
* @param \caption_field[] $fields
* @return array
*/
private function listRecordCaptionFields($fields)
{
$ret = [];
foreach ($fields as $field) {
$databox_field = $field->get_databox_field();
$fieldData = [
'meta_structure_id' => $field->get_meta_struct_id(),
'name' => $field->get_name(),
'labels' => [
'fr' => $databox_field->get_label('fr'),
'en' => $databox_field->get_label('en'),
'de' => $databox_field->get_label('de'),
'nl' => $databox_field->get_label('nl'),
],
];
foreach ($field->get_values() as $value) {
$data = [
'meta_id' => $value->getId(),
'value' => $value->getValue(),
];
$ret[] = $fieldData + $data;
}
}
return $ret;
}
/**
* Retrieve detailed information about one status
*
* @param \record_adapter $record
* @return array
*/
private function listRecordStatus(\record_adapter $record)
{
$ret = [];
foreach ($record->getStatusStructure() as $bit => $status) {
$ret[] = [
'bit' => $bit,
'state' => \databox_status::bitIsSet($record->getStatusBitField(), $bit),
];
}
return $ret;
}
/**
* @param \record_adapter $record
* @return array
*/
private function listRecordCaption(\record_adapter $record)
{
$includeBusiness = $this->getAclForUser()->can_see_business_fields($record->getDatabox());
$caption = [];
foreach ($record->get_caption()->get_fields(null, $includeBusiness) as $field) {
$caption[] = [
'meta_structure_id' => $field->get_meta_struct_id(),
'name' => $field->get_name(),
'value' => $field->get_serialized_values(';'),
];
}
return $caption;
}
/**
* Returns requested includes
*
* @param Request $request
* @return string[]
*/
private function resolveSearchIncludes(Request $request)
{
$includes = [
'results.stories.records'
];
if ($request->attributes->get('_extended', false)) {
if ($request->get('search_type') != SearchEngineOptions::RECORD_STORY) {
$includes = array_merge($includes, [
'results.stories.records.subdefs',
'results.stories.records.metadata',
'results.stories.records.caption',
'results.stories.records.status'
]);
}
else {
$includes = [ 'results.stories.caption' ];
}
$includes = array_merge($includes, [
'results.records.subdefs',
'results.records.metadata',
'results.records.caption',
'results.records.status'
]);
}
return $includes;
}
/**
* @param SearchEngineResult $result
* @param string[] $includes
* @param int $urlTTL
* @param int|null $story_max_records
* @return SearchResultView
*/
private function buildSearchView(SearchEngineResult $result, array $includes, $urlTTL, $story_max_records = null)
{
$references = new RecordReferenceCollection($result->getResults());
$records = new RecordCollection();
$stories = new RecordCollection();
foreach ($references->toRecords($this->getApplicationBox()) as $record) {
if ($record->isStory()) {
$stories[$record->getId()] = $record;
} else {
$records[$record->getId()] = $record;
}
}
$resultView = new SearchResultView($result);
if ($stories->count() > 0) {
$user = $this->getAuthenticatedUser();
$children = [];
foreach ($stories->getDataboxIds() as $databoxId) {
$storyIds = $stories->getDataboxRecordIds($databoxId);
$selections = $this->findDataboxById($databoxId)
->getRecordRepository()
->findChildren($storyIds, $user,1, $story_max_records);
$children[$databoxId] = array_combine($storyIds, $selections);
}
/** @var StoryView[] $storyViews */
$storyViews = [];
/** @var RecordView[] $childrenViews */
$childrenViews = [];
foreach ($stories as $index => $story) {
$storyView = new StoryView($story);
$selection = $children[$story->getDataboxId()][$story->getRecordId()];
$childrenView = $this->buildRecordViews($selection);
foreach ($childrenView as $view) {
$childrenViews[spl_object_hash($view)] = $view;
}
$storyView->setChildren($childrenView);
$storyViews[$index] = $storyView;
}
if (in_array('results.stories.thumbnail', $includes, true)) {
$subdefViews = $this->buildSubdefsViews($stories, ['thumbnail'], $urlTTL);
foreach ($storyViews as $index => $storyView) {
$storyView->setSubdefs($subdefViews[$index]);
}
}
if (in_array('results.stories.metadatas', $includes, true) ||
in_array('results.stories.caption', $includes, true)) {
$captions = $this->app['service.caption']->findByReferenceCollection($stories);
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($stories);
$this->buildCaptionViews($storyViews, $captions, $canSeeBusiness);
}
$allChildren = new RecordCollection();
foreach ($childrenViews as $index => $childrenView) {
$allChildren[$index] = $childrenView->getRecord();
}
$names = in_array('results.stories.records.subdefs', $includes, true) ? null : ['thumbnail'];
$subdefViews = $this->buildSubdefsViews($allChildren, $names, $urlTTL);
$technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($allChildren);
foreach ($childrenViews as $index => $recordView) {
$recordView->setSubdefs($subdefViews[$index]);
$recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index]));
}
if (array_intersect($includes, ['results.stories.records.metadata', 'results.stories.records.caption'])) {
$captions = $this->app['service.caption']->findByReferenceCollection($allChildren);
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($allChildren);
$this->buildCaptionViews($childrenViews, $captions, $canSeeBusiness);
}
$resultView->setStories($storyViews);
}
if ($records->count() > 0) {
$names = in_array('results.records.subdefs', $includes, true) ? null : ['thumbnail'];
$recordViews = $this->buildRecordViews($records);
$subdefViews = $this->buildSubdefsViews($records, $names, $urlTTL);
$technicalDatasets = $this->app['service.technical_data']->fetchRecordsTechnicalData($records);
foreach ($recordViews as $index => $recordView) {
$recordView->setSubdefs($subdefViews[$index]);
$recordView->setTechnicalDataView(new TechnicalDataView($technicalDatasets[$index]));
}
if (array_intersect($includes, ['results.records.metadata', 'results.records.caption'])) {
$captions = $this->app['service.caption']->findByReferenceCollection($records);
$canSeeBusiness = $this->retrieveSeeBusinessPerDatabox($records);
$this->buildCaptionViews($recordViews, $captions, $canSeeBusiness);
}
$resultView->setRecords($recordViews);
}
return $resultView;
}
/**
* @param Request $request
* @return SearchEngineResult
*/
private function doSearch(Request $request)
{
$options = SearchEngineOptions::fromRequest($this->app, $request);
$options->setFirstResult((int)($request->get('offset_start') ?: 0));
$options->setMaxResults((int)$request->get('per_page') ?: 10);
$this->getSearchEngine()->resetCache();
$search_result = $this->getSearchEngine()->query((string)$request->get('query'), $options);
$this->getUserManipulator()->logQuery($this->getAuthenticatedUser(), $search_result->getQueryText());
// log array of collectionIds (from $options) for each databox
$collectionsReferencesByDatabox = $options->getCollectionsReferencesByDatabox();
foreach ($collectionsReferencesByDatabox as $sbid => $references) {
$databox = $this->findDataboxById($sbid);
$collectionsIds = array_map(function(CollectionReference $ref){return $ref->getCollectionId();}, $references);
$this->getSearchEngineLogger()->log($databox, $search_result->getQueryText(), $search_result->getTotal(), $collectionsIds);
}
$this->getSearchEngine()->clearCache();
return $search_result;
}
/**
* @return SearchEngineInterface
*/
private function getSearchEngine()
{
return $this->app['phraseanet.SE'];
}
/**
* @return UserManipulator
*/
private function getUserManipulator()
{
return $this->app['manipulator.user'];
}
/**
* @return SearchEngineLogger
*/
private function getSearchEngineLogger()
{
return $this->app['phraseanet.SE.logger'];
}
/**
* @param Request $request
* @return int
*/
private function resolveSubdefUrlTTL(Request $request)
{
$urlTTL = $request->query->get('subdef_url_ttl');
if (null !== $urlTTL) {
return (int)$urlTTL;
}
return $this->getConf()->get(['registry', 'general', 'default-subdef-url-ttl']);
}
/**
* @param RecordCollection|\record_adapter[] $references
* @return RecordView[]
*/
private function buildRecordViews($references)
{
if (!$references instanceof RecordCollection) {
$references = new RecordCollection($references);
}
$recordViews = [];
foreach ($references as $index => $record) {
$recordViews[$index] = new RecordView($record);
}
return $recordViews;
}
/**
* @param RecordReferenceInterface[]|RecordReferenceCollection|DataboxGroupable $references
* @param array|null $names
* @param int $urlTTL
* @return SubdefView[][]
*/
private function buildSubdefsViews($references, array $names = null, $urlTTL)
{
$subdefGroups = $this->app['service.media_subdef']
->findSubdefsByRecordReferenceFromCollection($references, $names);
$fakeSubdefs = [];
foreach ($subdefGroups as $index => $subdefGroup) {
if (!isset($subdefGroup['thumbnail'])) {
$fakeSubdef = new \media_subdef($this->app, $references[$index], 'thumbnail', true, []);
$fakeSubdefs[spl_object_hash($fakeSubdef)] = $fakeSubdef;
$subdefGroups[$index]['thumbnail'] = $fakeSubdef;
}
}
$allSubdefs = $this->mergeGroupsIntoOneList($subdefGroups);
$allPermalinks = \media_Permalink_Adapter::getMany(
$this->app,
array_filter($allSubdefs, function (\media_subdef $subdef) use ($fakeSubdefs) {
return !isset($fakeSubdefs[spl_object_hash($subdef)]);
})
);
$urls = $this->app['media_accessor.subdef_url_generator']
->generateMany($this->getAuthenticatedUser(), $allSubdefs, $urlTTL);
$subdefViews = [];
/** @var \media_subdef $subdef */
foreach ($allSubdefs as $index => $subdef) {
$subdefView = new SubdefView($subdef);
if (isset($allPermalinks[$index])) {
$subdefView->setPermalinkView(new PermalinkView($allPermalinks[$index]));
}
$subdefView->setUrl($urls[$index]);
$subdefView->setUrlTTL($urlTTL);
$subdefViews[spl_object_hash($subdef)] = $subdefView;
}
$reorderedGroups = [];
/** @var \media_subdef[] $subdefGroup */
foreach ($subdefGroups as $index => $subdefGroup) {
$reordered = [];
foreach ($subdefGroup as $subdef) {
$reordered[] = $subdefViews[spl_object_hash($subdef)];
}
$reorderedGroups[$index] = $reordered;
}
return $reorderedGroups;
}
/**
* @param array $groups
* @return array|mixed
*/
private function mergeGroupsIntoOneList(array $groups)
{
// Strips keys from the internal array
array_walk($groups, function (array &$group) {
$group = array_values($group);
});
if ($groups) {
return call_user_func_array('array_merge', $groups);
}
return [];
}
/**
* @param RecordReferenceInterface[]|DataboxGroupable $references
* @return array<int, bool>
*/
private function retrieveSeeBusinessPerDatabox($references)
{
if (!$references instanceof DataboxGroupable) {
$references = new RecordReferenceCollection($references);
}
$acl = $this->getAclForUser();
$canSeeBusiness = [];
foreach ($references->getDataboxIds() as $databoxId) {
$canSeeBusiness[$databoxId] = $acl->can_see_business_fields($this->findDataboxById($databoxId));
}
$rights = [];
foreach ($references as $index => $reference) {
$rights[$index] = $canSeeBusiness[$reference->getDataboxId()];
}
return $rights;
}
/**
* @param RecordView[] $recordViews
* @param \caption_record[] $captions
* @param bool[] $canSeeBusiness
*/
private function buildCaptionViews($recordViews, $captions, $canSeeBusiness)
{
foreach ($recordViews as $index => $recordView) {
$caption = $captions[$index];
$captionView = new CaptionView($caption);
$captionView->setFields($caption->get_fields(null, isset($canSeeBusiness[$index]) && (bool)$canSeeBusiness[$index]));
$recordView->setCaption($captionView);
}
}
}

View File

@@ -450,6 +450,51 @@ class LightboxController extends Controller
return $this->app->json($data);
}
/**
* @param Basket $basket
* @return Response
*/
public function ajaxGetElementsAction(Basket $basket)
{
$ret = [
'error' => false,
'datas' => [
'counts' => [
'yes' => 0,
'no' => 0,
'nul' => 0,
'total' => 0
]
]
];
try {
if (!$basket->getValidation()) {
throw new Exception('There is no validation session attached to this basket');
}
foreach ($basket->getElements() as $element) {
$vd = $element->getUserValidationDatas($this->getAuthenticatedUser());
if($vd->getAgreement() === true) {
$ret['datas']['counts']['yes']++;
}
elseif($vd->getAgreement() === false) {
$ret['datas']['counts']['no']++;
}
elseif($vd->getAgreement() === null) {
$ret['datas']['counts']['nul']++;
}
$ret['datas']['counts']['total']++;
}
}
catch (Exception $e) {
$ret = [
'error' => true,
'datas' => $e->getMessage()
];
}
return $this->app->json($ret);
}
/**
* @param Basket $basket
* @throws Exception

View File

@@ -75,6 +75,9 @@ class EditController extends Controller
'format' => '',
'explain' => '',
'tbranch' => $meta->get_tbranch(),
'generate_cterms' => $meta->get_generate_cterms(),
'gui_editable' => $meta->get_gui_editable(),
'gui_visible' => $meta->get_gui_visible(),
'maxLength' => $meta->get_tag()
->getMaxLength(),
'minLength' => $meta->get_tag()

View File

@@ -15,6 +15,7 @@ use Alchemy\Phrasea\Application\Helper\FilesystemAware;
use Alchemy\Phrasea\Application\Helper\NotifierAware;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Core\Event\ExportFailureEvent;
use Alchemy\Phrasea\Core\Event\ExportMailEvent;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
@@ -193,42 +194,26 @@ class ExportController extends Controller
$token = $this->getTokenManipulator()->createEmailExportToken(serialize($list));
if (count($destMails) > 0) {
//zip documents
\set_export::build_zip(
$this->app,
$token,
$list,
$this->app['tmp.download.path'].'/'. $token->getValue() . '.zip'
);
$emitterId = $this->getAuthenticatedUser()->getId();
$remaingEmails = $destMails;
$tokenValue = $token->getValue();
$url = $this->app->url('prepare_download', ['token' => $token->getValue(), 'anonymous' => false, 'type' => \Session_Logger::EVENT_EXPORTMAIL]);
$user = $this->getAuthenticatedUser();
$emitter = new Emitter($user->getDisplayName(), $user->getEmail());
$params = [
'url' => $url,
'textmail' => $request->request->get('textmail'),
'reading_confirm' => !!$request->request->get('reading_confirm', false),
'ssttid' => $ssttid = $request->request->get('ssttid', ''),
'lst' => $lst = $request->request->get('lst', ''),
];
foreach ($destMails as $key => $mail) {
try {
$receiver = new Receiver(null, trim($mail));
} catch (InvalidArgumentException $e) {
continue;
}
$mail = MailRecordsExport::create($this->app, $receiver, $emitter, $request->request->get('textmail'));
$mail->setButtonUrl($url);
$mail->setExpiration($token->getExpiration());
$this->deliver($mail, !!$request->request->get('reading_confirm', false));
unset($remaingEmails[$key]);
}
//some mails failed
if (count($remaingEmails) > 0) {
foreach ($remaingEmails as $mail) {
$this->dispatch(PhraseaEvents::EXPORT_MAIL_FAILURE, new ExportFailureEvent($this->getAuthenticatedUser(), $ssttid, $lst, \eventsmanager_notify_downloadmailfail::MAIL_FAIL, $mail));
}
}
$this->dispatch(PhraseaEvents::EXPORT_MAIL_CREATE, new ExportMailEvent(
$emitterId,
$tokenValue,
$destMails,
$params
));
}
return $this->app->json([

View File

@@ -19,8 +19,10 @@ use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Feed\Aggregate;
use Alchemy\Phrasea\Feed\Link\AggregateLinkGenerator;
use Alchemy\Phrasea\Feed\Link\FeedLinkGenerator;
use Alchemy\Phrasea\Model\Entities\Feed;
use Alchemy\Phrasea\Model\Entities\FeedEntry;
use Alchemy\Phrasea\Model\Entities\FeedItem;
use Alchemy\Phrasea\Model\Entities\FeedPublisher;
use Alchemy\Phrasea\Model\Repositories\FeedEntryRepository;
use Alchemy\Phrasea\Model\Repositories\FeedItemRepository;
use Alchemy\Phrasea\Model\Repositories\FeedPublisherRepository;
@@ -46,6 +48,7 @@ class FeedController extends Controller
}
public function createFeedEntryAction(Request $request) {
/** @var Feed $feed */
$feed = $this->getFeedRepository()->find($request->request->get('feed_id'));
if (null === $feed) {
@@ -53,6 +56,8 @@ class FeedController extends Controller
}
$user = $this->getAuthenticatedUser();
/** @var FeedPublisher $publisher */
$publisher = $this->getFeedPublisherRepository()->findOneBy([
'feed' => $feed,
'user' => $user,

View File

@@ -136,6 +136,7 @@ class LanguageController
'or' => $translator->trans('or'),
'Suppr' => $translator->trans('Suppr'),
'Add new range' => $translator->trans('Add new range'),
'Save as VTT' => $translator->trans('Save as VTT'),
'Export ranges' => $translator->trans('Export ranges'),
'Start Range' => $translator->trans('Start Range'),
'End Range' => $translator->trans('End Range'),

View File

@@ -126,6 +126,16 @@ class LazaretController extends Controller
$ret = $lazaretManipulator->add($file_id, $keepAttributes, $attributesToKeep);
try{
// get the new record
$record = \Collection::getByBaseId($this->app, $request->request->get('bas_id'))->get_databox()->get_record($ret['result']['record_id']);
$postStatus = (array) $request->request->get('status');
// update status
$this->updateRecordStatus($record, $postStatus);
}catch(\Exception $e){
$ret['message'] = $this->app->trans('An error occured when wanting to change status!');
}
return $this->app->json($ret);
}
@@ -216,6 +226,7 @@ class LazaretController extends Controller
return $this->app->json($ret);
}
$postStatus = (array) $request->request->get('status');
$path = $this->app['tmp.lazaret.path'] . '/';
$lazaretFileName = $path .$lazaretFile->getFilename();
@@ -233,6 +244,9 @@ class LazaretController extends Controller
''
);
// update status
$this->updateRecordStatus($record, $postStatus);
//Delete lazaret file
$manager = $this->getEntityManager();
$manager->remove($lazaretFile);
@@ -278,6 +292,35 @@ class LazaretController extends Controller
);
}
/**
* @param Request $request
* @param $databox_id
* @param $record_id
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function getDestinationStatus(Request $request, $databox_id, $record_id)
{
if (!$request->isXmlHttpRequest()) {
$this->app->abort(400);
}
$record = new \record_adapter($this->app, (int) $databox_id, (int) $record_id);
$databox = $this->findDataboxById($databox_id);
$statusStructure = $databox->getStatusStructure();
$recordsStatuses = [];
foreach ($statusStructure as $status) {
// make the key as a string for the json usage in javascript
$bit = "'".$status['bit']."'";
if (!isset($recordsStatuses[$bit])) {
$recordsStatuses[$bit] = $status;
}
$statusSet = \databox_status::bitIsSet($record->getStatusBitField(), $status['bit']);
if (!isset($recordsStatuses[$bit]['flag'])) {
$recordsStatuses[$bit]['flag'] = (int) $statusSet;
}
}
return $this->app->json(['status' => $recordsStatuses]);
}
/**
* @return LazaretFileRepository
*/
@@ -293,4 +336,32 @@ class LazaretController extends Controller
{
return $this->app['border-manager'];
}
/**
* Set new status to selected record
*
* @param \record_adapter $record
* @param array $postStatus
* @return array|null
*/
private function updateRecordStatus(\record_adapter $record, array $postStatus)
{
$sbasId = $record->getDataboxId();
if (isset($postStatus[$sbasId]) && is_array($postStatus[$sbasId])) {
$postStatus = $postStatus[$sbasId];
$currentStatus = strrev($record->getStatus());
$newStatus = '';
foreach (range(0, 31) as $i) {
$newStatus .= isset($postStatus[$i]) ? ($postStatus[$i] ? '1' : '0') : $currentStatus[$i];
}
$record->setStatus(strrev($newStatus));
$this->getDataboxLogger($record->getDatabox())
->log($record, \Session_Logger::EVENT_STATUS, '', '');
return [
'current_status' => $currentStatus,
'new_status' => $newStatus,
];
}
return null;
}
}

View File

@@ -12,7 +12,7 @@ namespace Alchemy\Phrasea\Controller\Prod;
use Alchemy\Phrasea\Application\Helper\DataboxLoggerAware;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Helper\Record as RecordHelper;
use Alchemy\Phrasea\Out\Module\PDF as PDFExport;
use Alchemy\Phrasea\Out\Module\PDFRecords;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -24,19 +24,27 @@ class PrinterController extends Controller
{
$printer = new RecordHelper\Printer($this->app, $request);
return $this->render('prod/actions/printer_default.html.twig', ['printer' => $printer, 'message' => '']);
$basketFeedbackId = null;
if($printer->is_basket() && ($basket = $printer->get_original_basket()) && ($validation = $basket->getValidation())) {
if($validation->getInitiator()->getId() === $this->app->getAuthenticatedUser()->getId()) {
$basketFeedbackId = $basket->getId();
}
}
return $this->render('prod/actions/printer_default.html.twig', ['printer' => $printer, 'message' => '', 'basketFeedbackId' => $basketFeedbackId]);
}
public function printAction(Request $request)
{
$printer = new RecordHelper\Printer($this->app, $request);
$b = $printer->get_original_basket();
$layout = $request->request->get('lay');
foreach ($printer->get_elements() as $record) {
$this->getDataboxLogger($record->getDatabox())->log($record, \Session_Logger::EVENT_PRINT, $layout, '');
}
$PDF = new PDFExport($this->app, $printer->get_elements(), $layout);
$PDF = new PDFRecords($this->app, $printer, $layout);
$response = new Response($PDF->render(), 200, array('Content-Type' => 'application/pdf'));
$response->headers->set('Pragma', 'public', true);
@@ -44,4 +52,5 @@ class PrinterController extends Controller
return $response;
}
}

View File

@@ -193,7 +193,7 @@ class PushController extends Controller
'Validation from %user%', [
'%user%' => $this->getAuthenticatedUser()->getDisplayName(),
]));
$validation_description = $request->request->get('validation_description');
$validation_description = $request->request->get('message');
$participants = $request->request->get('participants');
@@ -604,6 +604,38 @@ class PushController extends Controller
);
}
public function updateExpirationAction(Request $request)
{
$ret = [
'success' => false,
'message' => $this->app->trans('Unable to save the expiration date')
];
if (is_null($request->request->get('date'))) {
$ret['message'] = $this->app->trans('The provided date is null!');
return $this->app->json($ret);
}
$repository = $this->app['repo.baskets'];
$manager = $this->getEntityManager();
$manager->beginTransaction();
try {
$basket = $repository->findUserBasket($request->request->get('basket_id'), $this->app->getAuthenticatedUser(), true);
$date = new \DateTime($request->request->get('date') . " 23:59:59");
$validation = $basket->getValidation();
if (is_null($validation)) {
return $this->app->json($ret);
}
$validation->setExpires($date);
$manager->persist($validation);
$manager->flush();
$manager->commit();
$ret['message'] = $this->app->trans('Expiration date successfully updated!');
} catch (\Exception $e) {
$ret['message'] = $e->getMessage();
$manager->rollback();
}
return $this->app->json($ret);
}
private function formatUser(User $user)
{
$subtitle = array_filter([$user->getJob(), $user->getCompany()]);
@@ -738,4 +770,5 @@ class PushController extends Controller
{
return $this->app['random.medium'];
}
}

View File

@@ -269,7 +269,7 @@ class QueryController extends Controller
$infoResult = '<div id="docInfo">'
. $this->app->trans('%number% documents<br/>selectionnes', ['%number%' => '<span id="nbrecsel"></span>'])
. '<div class="detailed_info_holder"><img src="/assets/common/images/icons/dots.png" class="image-normal"><img src="/assets/common/images/icons/dots-darkgreen-hover.png" class="image-hover">'
. '<div class="detailed_info_holder"><img src="/assets/common/images/icons/dots.png" class="image-normal hidden"><img src="/assets/common/images/icons/dots-darkgreen-hover.png" class="image-hover">'
. '<div class="detailed_info">
<table>
<thead>
@@ -341,10 +341,20 @@ class QueryController extends Controller
if ($result->getTotal() === 0) {
$template = 'prod/results/help.html.twig';
} else {
}
else {
$template = 'prod/results/records.html.twig';
}
$json['results'] = $this->render($template, ['results'=> $result]);
/** @var \Closure $filter */
$filter = $this->app['plugin.filter_by_authorization'];
$plugins = [
'workzone' => $filter('workzone'),
'actionbar' => $filter('actionbar'),
];
$json['results'] = $this->render($template, ['results'=> $result, 'plugins'=>$plugins]);
// add technical fields
@@ -382,7 +392,6 @@ class QueryController extends Controller
'labels' => $field->get_labels(),
'type' => $field->get_type(),
'field' => $field->get_name(),
'query' => "field." . $field->get_name() . ":%s",
'trans_label' => $field->get_label($this->app['locale']),
];
$field->get_label($this->app['locale']);
@@ -424,24 +433,15 @@ class QueryController extends Controller
// populates facets (aggregates)
$facets = [];
// $facetClauses = [];
foreach ($result->getFacets() as $facet) {
$facetName = $facet['name'];
if(array_key_exists($facetName, $fieldsInfosByName)) {
$f = $fieldsInfosByName[$facetName];
$facet['label'] = $f['trans_label'];
$facet['labels'] = $f['labels'];
$facet['type'] = strtoupper($f['type']) . "-AGGREGATE";
$facets[] = $facet;
// $facetClauses[] = [
// 'type' => strtoupper($f['type']) . "-AGGREGATE",
// 'field' => $f['field'],
// 'facet' => $facet
// ];
}
}

View File

@@ -7,6 +7,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Alchemy\Phrasea\Controller\Prod;
use Alchemy\Phrasea\Application;
@@ -14,6 +15,8 @@ use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
use Alchemy\Phrasea\Application\Helper\SearchEngineAware;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Controller\RecordsRequest;
use Alchemy\Phrasea\Core\Event\Record\DeleteEvent;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Core\Event\RecordEdit;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Model\Entities\BasketElement;
@@ -22,6 +25,7 @@ use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
use Alchemy\Phrasea\Twig\Fit;
use Alchemy\Phrasea\Twig\PhraseanetExtension;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -34,7 +38,7 @@ class RecordController extends Controller
*
* @param Request $request
*
* @return Response
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function getRecord(Request $request)
{
@@ -192,7 +196,8 @@ class RecordController extends Controller
$flatten = (bool)($request->request->get('del_children')) ? RecordsRequest::FLATTEN_YES_PRESERVE_STORIES : RecordsRequest::FLATTEN_NO;
$records = RecordsRequest::fromRequest(
$this->app,
$request,$flatten,
$request,
$flatten,
[\ACL::CANDELETERECORD]
);
@@ -234,7 +239,7 @@ class RecordController extends Controller
if ($trashCollectionsBySbasId[$sbasId] !== null) {
if($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
// record is already in trash so delete it
$record->delete();
$this->getEventDispatcher()->dispatch(RecordEvents::DELETE, new DeleteEvent($record));
} else {
// move to trash collection
$record->move_to_collection($trashCollectionsBySbasId[$sbasId], $this->getApplicationBox());
@@ -247,7 +252,7 @@ class RecordController extends Controller
}
} else {
// no trash collection, delete
$record->delete();
$this->getEventDispatcher()->dispatch(RecordEvents::DELETE, new DeleteEvent($record));
}
} catch (\Exception $e) {
}
@@ -278,35 +283,69 @@ class RecordController extends Controller
* Delete a record or a list of records
*
* @param Request $request
* @return Response
* @return string html
*/
public function whatCanIDelete(Request $request)
{
$viewParms = [];
// pre-count records that would be trashed/deleted when the "deleted children" will be un-checked
$records = RecordsRequest::fromRequest(
$this->app,
$request,
!!$request->request->get('del_children'),
RecordsRequest::FLATTEN_NO,
[\ACL::CANDELETERECORD]
);
$filteredRecord = $this->filterRecordToDelete($records);
$filteredRecords = $this->filterRecordToDelete($records);
return $this->app->json([
'renderView' => $this->render('prod/actions/delete_records_confirm.html.twig', [
$viewParms['parents_only'] = [
'records' => $records,
'filteredRecord' => $filteredRecord
]),
'filteredRecord' => $filteredRecord
]);
'trashableCount' => count($filteredRecords['trash']),
'deletableCount' => count($filteredRecords['delete'])
];
// pre-count records that would be trashed/deleted when the "deleted children" will be checked
//
$records = RecordsRequest::fromRequest(
$this->app,
$request,
RecordsRequest::FLATTEN_YES_PRESERVE_STORIES,
[\ACL::CANDELETERECORD]
);
$filteredRecords = $this->filterRecordToDelete($records);
$viewParms['with_children'] = [
'records' => $records,
'trashableCount' => count($filteredRecords['trash']),
'deletableCount' => count($filteredRecords['delete'])
];
return $this->render(
'prod/actions/delete_records_confirm.html.twig',
$viewParms
);
}
/**
* classifies records in two groups (does NOT delete anything)
* - 'trash' : the record can go to trash because the db has a "_TRASH_" coll, and the record is not already into it
* - 'delete' : the record would be deleted because the db has no trash, or the record is already trashed
*
* @param RecordsRequest $records
* @return array
*/
private function filterRecordToDelete(RecordsRequest $records)
{
$ret = [
'trash' => [],
'delete' => []
];
$trashCollectionsBySbasId = [];
$goingToTrash = [];
$delete = [];
foreach ($records as $record) {
/** @var \record_adapter $record */
$sbasId = $record->getDatabox()->get_sbas_id();
if (!array_key_exists($sbasId, $trashCollectionsBySbasId)) {
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
@@ -314,21 +353,20 @@ class RecordController extends Controller
if ($trashCollectionsBySbasId[$sbasId] !== null) {
if ($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
// record is already in trash
$delete[] = $record;
$ret['delete'][] = $record;
}
else {
// will be moved to trash
$goingToTrash[] = $record;
$ret['trash'][] = $record;
}
}
else {
// trash does not exist
$delete[] = $record;
$ret['delete'][] = $record;
}
}
//check if all values in array are true
//return (!in_array(false, $goingToTrash, true));
return ['trash' => $goingToTrash, 'delete' => $delete];
return $ret;
}
/**
@@ -336,7 +374,8 @@ class RecordController extends Controller
*
* @param Request $request
*
* @return Response
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @throws \Alchemy\Phrasea\Cache\Exception
*/
public function renewUrl(Request $request)
{

View File

@@ -14,6 +14,8 @@ use Alchemy\Phrasea\Application\Helper\EntityManagerAware;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Controller\RecordsRequest;
use Alchemy\Phrasea\Controller\Exception as ControllerException;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent;
use Alchemy\Phrasea\Core\Event\RecordEdit;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Model\Entities\StoryWZ;
@@ -68,7 +70,9 @@ class StoryController extends Controller
break;
}
$story->set_metadatas($metadatas)->rebuild_subdefs();
$recordAdapter = $story->set_metadatas($metadatas);
// tell phraseanet to rebuild subdef
$this->dispatch(RecordEvents::SUBDEFINITION_CREATE, new SubdefinitionCreateEvent($recordAdapter));
$storyWZ = new StoryWZ();
$storyWZ->setUser($this->getAuthenticatedUser());

View File

@@ -16,6 +16,7 @@ use Alchemy\Phrasea\Application\Helper\SubDefinitionSubstituerAware;
use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Controller\RecordsRequest;
use Alchemy\Phrasea\Core\Event\Record\RecordEvents;
use Alchemy\Phrasea\Core\Event\Record\SubdefinitionCreateEvent;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Metadata\PhraseanetMetadataReader;
use Alchemy\Phrasea\Metadata\PhraseanetMetadataSetter;
@@ -156,7 +157,7 @@ class ToolsController extends Controller
}
if (!$substituted || $force) {
$record->rebuild_subdefs();
$this->dispatch(RecordEvents::SUBDEFINITION_CREATE, new SubdefinitionCreateEvent($record));
}
}

View File

@@ -181,8 +181,8 @@ class UploadController extends Controller
$uploadedFilename = $file->getRealPath();
$renamedFilename = null;
if(!empty($this->app['conf']->get(['main', 'storage', 'tmp_files']))) {
$tmpStorage = \p4string::addEndSlash($this->app['conf']->get(['main', 'storage', 'tmp_files'])).'upload/';
if(!empty($this->app['conf']->get(['main', 'storage', 'worker_tmp_files']))) {
$tmpStorage = \p4string::addEndSlash($this->app['conf']->get(['main', 'storage', 'worker_tmp_files'])).'upload/';
if(!is_dir($tmpStorage)){
$this->getFilesystem()->mkdir($tmpStorage);

View File

@@ -11,6 +11,7 @@
namespace Alchemy\Phrasea\Controller;
use Alchemy\Phrasea\Model\Converter\BasketConverter;
use Alchemy\Phrasea\Model\Entities\Basket;
use Doctrine\Common\Collections\ArrayCollection;
use Alchemy\Phrasea\Application;
@@ -21,6 +22,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class RecordsRequest extends ArrayCollection
{
protected $isSingleStory = false;
protected $rejected;
protected $received;
protected $basket;
protected $databoxes;
@@ -31,31 +33,41 @@ class RecordsRequest extends ArrayCollection
const FLATTEN_YES_PRESERVE_STORIES = 'preserve';
/**
* Constructor
* RecordsRequest Constructor
*
* @param array $elements
* @param ArrayCollection $rejected
* @param ArrayCollection $received
* @param Basket $basket
* @param Basket|null $basket
* @param Boolean $flatten
*/
public function __construct(array $elements, ArrayCollection $received, Basket $basket = null, $flatten = self::FLATTEN_NO)
public function __construct(array $elements, ArrayCollection $rejected, ArrayCollection $received, Basket $basket = null, $flatten = self::FLATTEN_NO)
{
parent::__construct($elements);
$this->received = $received;
$this->rejected = $rejected;
$this->basket = $basket;
$this->isSingleStory = ($flatten !== self::FLATTEN_YES && 1 === count($this) && $this->first()->isStory());
// since stories are already flattened by "fromRequest" (to apply rights on children),
// flagging "isSingleStory" is a bit more difficult than checking the first item...
// $this->isSingleStory = ($flatten !== self::FLATTEN_YES && count($this) === 1 && $this->first()->isStory());
if (self::FLATTEN_NO !== $flatten) {
if ($flatten !== self::FLATTEN_NO) {
$to_remove = [];
/** @var record_adapter $record */
foreach ($this as $key => $record) {
if ($record->isStory()) {
if (self::FLATTEN_YES === $flatten) {
if ($flatten === self::FLATTEN_YES) {
// simple flatten : remove the story
$to_remove[] = $key;
}
try {
foreach ($record->getChildren() as $child) {
$this->set($child->getId(), $child);
}
} catch (\Exception $e) {
// getChildren will no fail since record IS a story
}
}
}
@@ -64,11 +76,39 @@ class RecordsRequest extends ArrayCollection
}
}
// We check that the list contains only one story, and that every other items (records) are children of this story
// Too bad : there is no "isChildOf" method :(
$rec = [];
$children = [];
$this->isSingleStory = false;
$i = 0;
$records = $this->toArray();
array_walk($records, function (\record_adapter $record) use (&$i) {
foreach ($this as $key => $record) {
if($record->isStory()) {
if($this->isSingleStory) {
// we already found a story, we cannot have 2, game over
$this->isSingleStory = false;
break;
}
$this->isSingleStory = true;
foreach ($record->getChildren() as $child) {
$children[$child->getId()] = 1; // to later find by key
}
}
else {
$rec[] = $record->getId();
}
$record->setNumber($i++);
});
}
if($this->isSingleStory) {
foreach ($rec as $rid) {
if(!array_key_exists($rid, $children)) {
// one record is not a child, game over
$this->isSingleStory = false;
break;
}
}
}
}
/**
@@ -106,7 +146,7 @@ class RecordsRequest extends ArrayCollection
/** @var \record_adapter $record */
foreach ($this as $record) {
if (! isset($this->collections[$record->getBaseId()])) {
$this->collections[$record->getBaseId()] = $record->get_collection();
$this->collections[$record->getBaseId()] = $record->getCollection();
}
}
@@ -126,6 +166,16 @@ class RecordsRequest extends ArrayCollection
return $this->received;
}
/**
* Return all rejected records
*
* @return \record_adapter[]|ArrayCollection
*/
public function rejected()
{
return $this->rejected;
}
/**
* Return basket entity if provided, null otherwise
*
@@ -201,15 +251,18 @@ class RecordsRequest extends ArrayCollection
* @param boolean $flattenStories
* @param array $rightsColl
* @param array $rightsDatabox
* @return RecordsRequest|\record_adapter[]
* @return RecordsRequest
* @throws \Alchemy\Phrasea\Cache\Exception
*/
public static function fromRequest(Application $app, Request $request, $flattenStories = self::FLATTEN_NO, array $rightsColl = [], array $rightsDatabox = [])
{
$elements = $received = [];
$received = [];
$basket = null;
if ($request->get('ssel')) {
$basket = $app['converter.basket']->convert($request->get('ssel'));
/** @var BasketConverter $basketConverter */
$basketConverter = $app['converter.basket'];
$basket = $basketConverter->convert($request->get('ssel'));
$app['acl.basket']->hasAccess($basket, $app->getAuthenticatedUser());
foreach ($basket->getElements() as $basket_element) {
@@ -240,35 +293,56 @@ class RecordsRequest extends ArrayCollection
}
}
// fill an array with records from flattened stories
$elements = $received;
$to_remove = [];
if ($flattenStories !== self::FLATTEN_NO) {
/** @var record_adapter $record */
foreach ($received as $key => $record) {
if ($record->isStory()) {
if ($flattenStories === self::FLATTEN_YES) {
// simple flatten : remove the story from elements
unset($elements[$key]);
}
foreach ($record->getChildren() as $child) {
$elements[$child->getId()] = $child;
}
}
}
}
foreach ($elements as $id => $record) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_access_to_record($record)) {
$to_remove[] = $id;
continue;
// apply rights filter, remove from elements if no rights
$rejected = [];
$acl = $app->getAclForUser($app->getAuthenticatedUser());
foreach ($elements as $key => $record) {
// any false or unknown right will throw exception and the record will be rejected
try {
if (!$acl->has_access_to_record($record)) {
throw new \Exception();
}
foreach ($rightsColl as $right) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_right_on_base($record->get_base_id(), $right)) {
$to_remove[] = $id;
continue;
if (!$acl->has_right_on_base($record->getBaseId(), $right)) {
throw new \Exception();
}
}
foreach ($rightsDatabox as $right) {
if (!$app->getAclForUser($app->getAuthenticatedUser())->has_right_on_sbas($record->get_sbas_id(), $right)) {
$to_remove[] = $id;
continue;
if (!$acl->has_right_on_sbas($record->getDataboxId(), $right)) {
throw new \Exception();
}
}
}
catch (\Exception $e) {
$rejected[$key] = $record;
}
}
// remove rejected from elements
foreach ($rejected as $key => $record) {
unset($elements[$key]);
}
foreach ($to_remove as $id) {
unset($elements[$id]);
}
return new static($elements, new ArrayCollection($received), $basket, $flattenStories);
// flattening is already done
return new static($elements, new ArrayCollection($rejected), new ArrayCollection($received), $basket, self::FLATTEN_NO);
}
}

View File

@@ -458,9 +458,9 @@ class AccountController extends Controller
->setZipCode($request->request->get("form_zip"))
->setPhone($request->request->get("form_phone"))
->setFax($request->request->get("form_fax"))
->setJob($request->request->get("form_activity"))
->setJob($request->request->get("form_function"))
->setCompany($request->request->get("form_company"))
->setPosition($request->request->get("form_function"))
->setPosition($request->request->get("form_activity"))
->setNotifications((Boolean) $request->request->get("mail_notifications"));
$service->updateAccount($command);
@@ -518,7 +518,9 @@ class AccountController extends Controller
$this->getApiApplicationManipulator()->deleteApiApplications($applications);
// revoke access and delete phraseanet user account
// get list of old granted base_id then revoke access and delete phraseanet user account
$oldGrantedBaseIds = array_keys($this->app->getAclForUser($user)->get_granted_base());
$list = array_keys($this->app['repo.collections-registry']->getBaseIdMap());
@@ -542,8 +544,9 @@ class AccountController extends Controller
$mail = null;
}
$this->app['manipulator.user']->delete($user);
$mail = MailSuccessAccountDelete::create($this->app, $receiver);
$this->app['manipulator.user']->delete($user, [$user->getId() => $oldGrantedBaseIds]);
if($mail) {
$this->deliver($mail);
}

View File

@@ -174,7 +174,9 @@ class SetupController extends Controller
$email = $request->request->get('email');
$password = $request->request->get('password');
$template = $request->request->get('db_template');
$dataPath = $request->request->get('datapath_noweb');
$storagePath = [
'subdefs' => $request->request->get('datapath_noweb')
];
try {
$installer = $this->app['phraseanet.installer'];
@@ -193,7 +195,7 @@ class SetupController extends Controller
$binaryData[$key] = $path;
}
$user = $installer->install($email, $password, $abConn, $servername, $dataPath, $dbConn, $template, $binaryData);
$user = $installer->install($email, $password, $abConn, $servername, $storagePath, $dbConn, $template, $binaryData);
$this->app->getAuthenticator()->openAccount($user);

View File

@@ -0,0 +1,48 @@
<?php
namespace Alchemy\Phrasea\ControllerProvider\Api;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\Controller\Api\V3Controller;
use Alchemy\Phrasea\Core\Event\Listener\OAuthListener;
use Silex\Application;
use Silex\ControllerCollection;
use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface;
class V3 extends Api implements ControllerProviderInterface, ServiceProviderInterface
{
const VERSION = '3.0.0';
public function register(Application $app)
{
$app['controller.api.v3'] = $app->share(function (PhraseaApplication $app) {
return (new V3Controller($app));
});
}
public function boot(Application $app)
{
}
public function connect(Application $app)
{
if (! $this->isApiEnabled($app)) {
return $app['controllers_factory'];
}
/** @var ControllerCollection $controllers */
$controllers = $app['controllers_factory'];
$controllers->before(new OAuthListener());
$controllers->get('/stories/{databox_id}/{record_id}/', 'controller.api.v3:getStoryAction')
->before('controller.api.v1:ensureCanAccessToRecord')
->assert('databox_id', '\d+')
->assert('record_id', '\d+');
$controllers->match('/search/', 'controller.api.v3:searchAction');
return $controllers;
}
}

View File

@@ -105,6 +105,11 @@ class Lightbox implements ControllerProviderInterface, ServiceProviderInterface
->assert('basket', '\d+')
;
$controllers->get('/ajax/GET_ELEMENTS/{basket}/', 'controller.lightbox:ajaxGetElementsAction')
->bind('lightbox_ajax_get_elements')
->assert('basket', '\d+')
;
return $controllers;
}

View File

@@ -82,6 +82,11 @@ class Lazaret implements ControllerProviderInterface, ServiceProviderInterface
->assert('file_id', '\d+')
->bind('lazaret_thumbnail');
$controllers->get('/{databox_id}/{record_id}/status', 'controller.prod.lazaret:getDestinationStatus')
->assert('databox_id', '\d+')
->assert('record_id', '\d+')
->bind('lazaret_destination_status');
return $controllers;
}
}

View File

@@ -59,6 +59,9 @@ class Push implements ControllerProviderInterface, ServiceProviderInterface
$controllers->post('/validate/', 'controller.prod.push:validateAction')
->bind('prod_push_validate');
$controllers->post('/update-expiration/', 'controller.prod.push:updateExpirationAction')
->bind('prod_push_do_update_expiration');
$controllers->get('/user/{usr_id}/', 'controller.prod.push:getUserAction')
->assert('usr_id', '\d+');

View File

@@ -20,6 +20,7 @@ class DisplaySettingService
const ORDER_BY_ADMIN = "ORDER_BY_ADMIN";
const ORDER_BY_BCT = "ORDER_BY_BCT";
const ORDER_BY_HITS = "ORDER_BY_HITS";
const ORDER_BY_HITS_ASC = "ORDER_BY_HITS_ASC";
/**
* The default user settings.
@@ -31,7 +32,7 @@ class DisplaySettingService
'images_per_page' => '20',
'images_size' => '120',
'editing_images_size' => '134',
'editing_top_box' => '180px',
'editing_top_box' => '120px',
'editing_right_box' => '400px',
'editing_left_box' => '710px',
'basket_sort_field' => 'name',

Some files were not shown because too many files have changed in this diff Show More