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

This commit is contained in:
Nicolas Maillat
2019-09-27 16:40:56 +02:00
committed by GitHub
130 changed files with 8605 additions and 733 deletions

View File

@@ -15,7 +15,7 @@ jobs:
CIRCLE_TEST_REPORTS: /tmp/circleci-test-results
docker:
- image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37
command: /sbin/init
- image: circleci/rabbitmq:3.7.7
steps:
- checkout
- run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS
@@ -24,10 +24,7 @@ jobs:
command: nvm install v10.12.0 && nvm alias default v10.12.0
- run:
working_directory: ~/alchemy-fr/Phraseanet
command: 'sudo service memcached status || sudo service memcached start; sudo
redis-cli ping >/dev/null 2>&1 || sudo service redis-server start; sudo
service mysql status || sudo service mysql start; sudo service rabbitmq-server
status || sudo service rabbitmq-server start; '
command: 'sudo service mysql status || sudo service mysql start;'
# Dependencies
# This would typically go in either a build or a build-and-test job when using workflows
# Restore the dependency cache
@@ -124,7 +121,7 @@ workflows:
context: "AWS London"
create-repo: true
dockerfile: Dockerfile
extra-build-args: "--target phraseanet"
extra-build-args: "--target phraseanet-fpm"
region: AWS_DEFAULT_REGION
repo: "${AWS_RESOURCE_NAME_PREFIX}/phraseanet"
tag: "alpha-0.1"
@@ -139,3 +136,14 @@ workflows:
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
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"

View File

@@ -1,14 +1,140 @@
# CHANGELOG
## 4.0.9
## 4.0.0 (xxxx-xx-xx)
### Adds
- Convert Orders custom adapter to Doctrine entity.
- Convert Feeds custom adapter to Doctrine entity.
- Convert Users custom adapter to Doctrine entity.
- Convert Ftp Export custom adapter to Doctrine entity.
- Convert Ftp Export custom adapter to Doctrine entity.
- Session management is now part of Phraseanet configuration.
- PHRAS-2535 - Back / Front - Unsubscription: It's now possible to request a validation by email to delete a Phraseanet user account.
- PHRAS-2480 - Back / Front - It's now possible to add a user model as order manager on a collection:All users with this model applied can manage orders on this collection. This features fixes an issue when users is provided by SAML and the orders manager is lost when user logs in.
- PHRAS-2474 - Back / front. - Searched terms are now found even if the searched terms are split in Business Field and regular Field.
- PHRAS-2462 - Front - Share media on LinkedIn as you can do on Facebook, Twitter.
- PHRAS-2417 - Front - Skin: grey and white, graphic enhancements.
- PHRAS-2067 - Front - Introducing thumbnail & preview generic images for Fonts
### Fixes
* PHRAS-2491 - Front - Click on facets title (expand/collapse) launched a bad query, due to jquery error.
* PHRAS-2510 - Front - Facets values appear Truncated after 15th character.
* PHRAS-2153 - Front - No user search possible with the field "Company" and field "Country".
* PHRAS-2154 - Front - Bug on Chrome only - selected 1 document instead of all for the feedback.
* PHRAS-2538 - Back - Some MP4 files were not correctly detected by Phraseanet.
## 4.0.8
### Adds:
- Upload: Distant files can be added via their URL in GUI and by API. Phraseanet downloads the file before archiving it.
- Search optimisation when searching in full text, there was a problem when the query mixed different types of fields.
- Search optimisation, its now possible to search a partial date in full text.
- Populate optimisation, now populating time: 3 times faster.
- It is now possible to migrate from 3.1 3.0 version to 4.X, without an intermediate step in 3.8.Fix:
### Fixes
- Search filter were not taken into account due to a bug in JS.
- Overlay title: In this field, text was repeated twice if : one or several words were highlighted in the field, and if the title contained more than 102 characters.
- List Manager: it was impossible to add users in the list manager after page 3.
- List of fields was not refreshed in the exported fields section.
- Push and Feedback fix error when adding a user when Geonames was not set (null value in Geonames).
## 4.0.7
### Adds:
- Advanced search refacto
- Thesaurus search is now in strict mode
- Refactoring of report module
- Refactoring query storage and changing strategy for field search restriction
- It is now possible to search for terms in thesaurus and candidates in all languages, not only on the login language
- Enhancements on archive task
- Graphic enhancements for menu and icons
- Video file enhancement, support of MXF container
- Extraction of a video soundtrack (MP3, MP4, WAVE, etc.)
- For Office Documents, all generated subviews will be PDF assets by default. The flexpaper preview still exists but will be optional.
- In Prod Gui, there will be 5 facets but the possibility to view more.
### Fixes:
- Quarantine: Fix for the “Substitute” action: alert when selection is empty
- Quarantine: File name with a special character cant be added
- Fix for the Adobe CC default token
- XSS vulnerabilities in Prod, Admin & Lightbox. Many thanks to Kris (@HV_hat_)
- PDF containing (XMP-xmp:PageImage) fails generating subview
- MIME types are trucated
-Vagrant dev environment fix
- Feedback: Sort assets “Order by best choice” has no effect
## 4.0.3
### Adds:
- Prod: For a record, show the current day in the statistics section of the detailed view.
- Prod: Store state (open or closed) of facet answer. eg: Database or collection, store in session.
- Admin: Access to scheduler and task local menu when parameter is set to false in .yml configuration.
- Prod: Database, collection and document type facets are fixed on top
- Prod: Better rendering for values of exposure, shutter speed and flash status in facets. eg for shutter speed: 1/30 instead of 0,0333333.
- Versions 4 are now compliant with the Phraseanet plugins for Adobe CC Suite.
- White list mode: extending autoregistration and adding wildcard access condition by mail domain. Automatically grant access to a user according to the email entered in the request.
- Find your documents from the colors in the facets (AI plugin)
- Generate a PDF from a Word document or a picture, its now possible to define a pdf subview type
- Specify a temporary work repository for building video subdefs, to accelerate video generation.
### Fixes:
- Prod: In Upload, correct status are not loaded
- Prod:Arrow keys navigation adds last selected facet as filter
- Admin:Subdef presets, sizes and bitrates (bits/s) not OK
- Admin: App error on loading in French due to a simple quote
- Prod: Deletion message is not fully readable when deleting a story
- Fixing highlight with Elasticsearch for full text only, not for the thesaurus
- 500 error at the first authentication for a user with the SAML Phraseanet pluginDev
- Dev: Fix API version returned in answer
- Dev: Fix vagrant provisioning for Windows
## 4.0.2
### Adds:
- Prod: Message Improv, when selected records are in Trash and another one.
- Prod: alt-click on active facets (filter) to invert it.
- Prod: do not erase facets in filter when returning 0 answers.
- Core: Add preference to authorize user connection without an email
- Core: Add preference to set default validity period of download link
### Fixes:
- Thesaurus: 0 character terms are blocked
- Admin: fix action create and drop index from elasticsearch
- Prod: Fix advanced sarch: no filters possible on fields using IE
- Prod: 500 error in publication reader when record is missing (deleted from db)Unit test: fix error in Json serialization for custom link
- Prod: fix field list in advanced search with Edge browser
- Upload: fix 500 error when missing collection
- Install wizard: fix error in graphical installer
## 4.0.0
### Adds:
#### Phraseanet gets a new search engine: Elasticsearch
- Faceted navigation enables to create a “mapping” of the response. Browse in a very intuitive way by creating several associations of filters. Facets can be used on the databases, collections, documentary fields and technical data.
- Speed of processing search and results display has been improved
- Possibility to use Kibana (open source visualization plugin for Elasticsearch)
#### API enhancement
- New API routes are available (orders, facets, quarantine)
- Enhancement of new, faster routes
#### Redesign of the Prod interface
- Enhanced, redesigned ergonomics: the detailed view windows; redesign of the workzone (baskets and stories, facets, webgalleries)
- New white and grey skins are now available
- New order manager
#### Other
- Permalinks sharing: activate/deactivate sharing links for the document and sub resolutions
- New: the applicative trash: you can now define a collection named _TRASH_. Then, all deleted records from collections (except from Trash) go to the Trash collection. Permalinks on subdefs are deactivated. When you delete a record from the Trash collection, it is permanently deleted. When you move a record from the Trash collection to another, the permalinks are reactivated.
- Rewriting of the task scheduler based on the web sockets
- Quarantine enhancement
- Drag and drop upload
## 3.8.8 (2015-12-02)

View File

@@ -40,7 +40,7 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" \
&& 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');"
@@ -88,7 +88,7 @@ COPY templates /var/alchemy/templates
COPY tests /var/alchemy/tests
# Phraseanet
FROM php:7.0-fpm-stretch as phraseanet
FROM php:7.0-fpm-stretch as phraseanet-fpm
RUN apt-get update \
&& apt-get install -y \
apt-transport-https \
@@ -152,6 +152,10 @@ WORKDIR /var/alchemy/Phraseanet
ENTRYPOINT ["/phraseanet-entrypoint.sh"]
CMD ["/boot.sh"]
# phraseanet-worker
FROM phraseanet-fpm as phraseanet-worker
CMD ["/worker-boot.sh"]
# phraseanet-nginx
FROM nginx:1.15 as phraseanet-nginx
RUN useradd -u 1000 app

View File

@@ -10,6 +10,10 @@ Phraseanet 4.1 - Digital Asset Management application
- Elasticsearch search engine
- Multiple resolution assets generation
# License :
Phraseanet is licensed under GPL-v3 license.
# Documentation :
https://docs.phraseanet.com/
@@ -31,34 +35,42 @@ https://www.phraseanet.com/download/
# 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.
- git clone
- vagrant up
git clone
vagrant up --provision
then, a prompt allow you to choose PHP version, and another one to choose a complete build or an Alchemy prebuilt boxes.
Ex:
- vagrant up --provision //// 5.6 ///// 1 >> Build an ubuntu/xenial box with php5.6
- vagrant up --provision //// 7.0 ///// 1 >> Build an ubuntu/xenial with php7.0
- vagrant up --provision //// 7.2 ///// 2 >> Build the alchemy/phraseanet-php-7.2 box
- 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
# License :
Phraseanet is licensed under GPL-v3 license.
# Docker build
WARNING : still in a work-in-progress status and can be used only for test purposes.
The docker distribution come with 2 differents containers :
* an nginx that act as the front http server.
* the php-fpm who serves the php files through nginx.
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 two images can be built respectively with these two commands :
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 -t local/phraseanet .
docker build --target phraseanet-fpm -t local/phraseanet-fpm .
# worker
docker build --target phraseanet-worker -t local/phraseanet-worker .

110
Vagrantfile vendored
View File

@@ -1,16 +1,6 @@
Vagrant.require_version ">= 1.5"
require 'json'
class MyCustomError < StandardError
attr_reader :code
def initialize(code)
@code = code
end
def to_s
"[#{code} #{super}]"
end
end
# Check to determine whether we're on a windows or linux/os-x host,
# later on we use this to launch ansible in the supported way
# source: https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
@@ -34,15 +24,62 @@ else if which('ifconfig')
end
end
$php = [ "5.6", "7.0", "7.1", "7.2" ]
$phpVersion = ENV['phpversion'] ? ENV['phpversion'] : "7.0";
unless Vagrant.has_plugin?('vagrant-hostmanager')
raise "vagrant-hostmanager is not installed! Please run\n vagrant plugin install vagrant-hostmanager\n\n"
end
unless $php.include?($phpVersion)
raise "You should specify php version before running vagrant\n\n (Available : 5.6, 7.0, 7.1, 7.2 | default => 5.6)\n\n Exemple: phpversion='7.0' vagrant up \n\n"
# Check to determine if box_meta JSON is present
# if provisionned : pick name of box
if File.file?(".vagrant/machines/default/virtualbox/box_meta")
data = File.read(".vagrant/machines/default/virtualbox/box_meta")
parsed_json = JSON.parse(data)
$box = parsed_json["name"]
end
# if not : run prompt to configure provisioning
if !File.file?(".vagrant/machines/default/virtualbox/box_meta") && ARGV[0] == 'up'
print "\033[34m \nChoose a Build type :\n\n(1) Use prebuilt Phraseanet Box\n(2) Build Phraseanet from scratch (xenial)\n\033[00m"
type = STDIN.gets.chomp
print "\n"
# Switch between Phraseanet box and native trusty64
case (type)
when '1'
$box = "alchemy/Phraseanet-vagrant-dev_php"
$playbook = "resources/ansible/playbook-boxes.yml"
when '2'
$box = "ubuntu/xenial64"
$playbook = "resources/ansible/playbook.yml"
else
raise "\033[31mYou should specify Build type before running vagrant\n\n (Available : 1, 2)\n\n\033[00m"
end
print "\033[32m-----------------------------------------------\n"
print "Build with "+$box+" box\n"
print "-----------------------------------------------\n\n\033[00m"
print "\033[34mChoose a PHP version for your build (Available : 5.6, 7.0, 7.1, 7.2)\n\033[00m"
phpversion = STDIN.gets.chomp
print "\n"
# Php version selection
case (phpversion)
when "5.6", "7.0", "7.1", "7.2"
print "\033[32mSelected PHP version : "+phpversion+"\n\033[00m"
print "Continue ? (Y/n) \n"
continue = STDIN.gets.chomp
case continue
when 'n', 'no', 'N', 'NO'
raise "\033[31mBuild aborted\033[00m"
else
if (type == '1')
$box.concat(phpversion)
end
print "\033[32m-----------------------------------------------\n"
print "Build with PHP"+phpversion+"\n"
print "-----------------------------------------------\n\n\033[00m"
end
else
raise "\033[31mYou should specify php version before running vagrant\n\n (Available : 5.6, 7.0, 7.1, 7.2)\n\n\033[00m"
end
end
$root = File.dirname(File.expand_path(__FILE__))
@@ -95,8 +132,6 @@ else if $env == "linux"
$hostIps = `ifconfig | sed -nE 's/[[:space:]]*inet ([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})(.*)$/\\1/p'`.split("\n");
else
$hostIps = `resources/ansible/inventories/GetIpAdresses.cmd`;
# raise MyCustomError.new($hostIps), "HOST IP"
end
end
@@ -119,33 +154,28 @@ Vagrant.configure("2") do |config|
]
end
# Switch between Phraseanet box and native trusty64
config.vm.box = "alchemy/Phraseanet-vagrant-dev"
#config.vm.box = "ubuntu/trusty64"
# In case, Phraseanet box, choose the php version
# For php 7.0 use box 0.0.1
# For php 7.1 use box 0.0.2
config.vm.box_version = "0.0.1"
config.vm.box = $box
config.ssh.forward_agent = true
config_net(config)
# If ansible is in your path it will provision from your HOST machine
# If ansible is not found in the path it will be instaled in the VM and provisioned from there
if which('ansible-playbook')
config.vm.provision "ansible_local" do |ansible|
ansible.playbook = "resources/ansible/playbook.yml"
ansible.limit = 'all'
ansible.verbose = 'vvv'
ansible.extra_vars = {
hostname: $hostname,
host_addresses: $hostIps,
phpversion: $phpVersion,
postfix: {
postfix_domain: $hostname + ".vb"
if $playbook
config.vm.provision "ansible_local" do |ansible|
ansible.playbook = $playbook
ansible.limit = 'all'
ansible.verbose = 'vvv'
ansible.extra_vars = {
hostname: $hostname,
host_addresses: $hostIps,
phpversion: phpversion,
postfix: {
postfix_domain: $hostname + ".vb"
}
}
}
end
end
config.vm.provision "ansible_local", run: "always" do |ansible|
@@ -158,10 +188,6 @@ Vagrant.configure("2") do |config|
}
end
else
# raise MyCustomError.new([$hostname, $phpVersion, $hostIps]), "HOST IP"
# raise MyCustomError.new($hostIps), "HOST IP"
# raise MyCustomError.new($hostIps), "HOST IP"
config.vm.provision :shell, path: "resources/ansible/windows.sh", args: [$hostname, $phpVersion, $hostIps]
# config.vm.provision :shell, run: "always", path: "resources/ansible/windows-always.sh", args: ["default"]
end

View File

@@ -24,6 +24,7 @@ use Alchemy\Phrasea\Command\Plugin\AddPlugin;
use Alchemy\Phrasea\Command\Plugin\RemovePlugin;
use Alchemy\Phrasea\Command\Plugin\EnablePlugin;
use Alchemy\Phrasea\Command\Plugin\DisablePlugin;
use Alchemy\Phrasea\Command\Plugin\DownloadPlugin;
use Alchemy\Phrasea\CLI;
use Alchemy\Phrasea\Command\Setup\CheckEnvironment;
use Alchemy\Phrasea\Core\CLIProvider\DoctrineMigrationServiceProvider;
@@ -50,7 +51,7 @@ $app = new CLI("
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; type `about:license' for details.\n\n"
. ' SETUP', $version->getName() . ' ' . $version->getNumber());
. ' SETUP', $version->getName() . ' ' . $version->getNumber());
$app->register(new DoctrineMigrationServiceProvider());
@@ -70,6 +71,7 @@ if ($configurationTester->isInstalled()) {
}
$app->command(new AddPlugin());
$app->command(new DownloadPlugin());
$app->command(new ListPlugin());
$app->command(new RemovePlugin());
$app->command(new PluginsReset());

0
cache/.gitkeep vendored
View File

View File

@@ -47,7 +47,7 @@
"php": ">=5.5.9",
"ext-intl": "*",
"alchemy-fr/tcpdf-clone": "~6.0",
"alchemy/embed-bundle": "^2.0.4",
"alchemy/embed-bundle": "^2.0.7",
"alchemy/geonames-api-consumer": "~0.1.0",
"alchemy/mediavorus": "^0.4.4",
"alchemy/oauth2php": "1.1.0",
@@ -120,7 +120,8 @@
"google/recaptcha": "^1.1",
"facebook/graph-sdk": "^5.6",
"box/spout": "^2.7",
"paragonie/random-lib": "^2.0"
"paragonie/random-lib": "^2.0",
"czproject/git-php": "^3.17"
},
"require-dev": {
"mikey179/vfsstream": "~1.5",

66
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": "a40bfa0aa6310530dc0c92b141b21305",
"content-hash": "f3b1fc0a30bf14b05e57ce673550d9c0",
"packages": [
{
"name": "alchemy-fr/tcpdf-clone",
@@ -131,16 +131,16 @@
},
{
"name": "alchemy/embed-bundle",
"version": "2.0.4",
"version": "2.0.7",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/embed-bundle.git",
"reference": "b510748686c05c0c1d59b7ad15e2c1098abafc5a"
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/b510748686c05c0c1d59b7ad15e2c1098abafc5a",
"reference": "b510748686c05c0c1d59b7ad15e2c1098abafc5a",
"url": "https://api.github.com/repos/alchemy-fr/embed-bundle/zipball/c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
"reference": "c585ccf18e53a9a6f2b696ddbbc39521732dfdde",
"shasum": ""
},
"require-dev": {
@@ -178,10 +178,10 @@
],
"description": "Embed resources bundle",
"support": {
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.4",
"source": "https://github.com/alchemy-fr/embed-bundle/tree/2.0.7",
"issues": "https://github.com/alchemy-fr/embed-bundle/issues"
},
"time": "2019-06-03T13:35:50+00:00"
"time": "2019-09-02T12:28:19+00:00"
},
{
"name": "alchemy/geonames-api-consumer",
@@ -383,16 +383,16 @@
},
{
"name": "alchemy/phpexiftool",
"version": "0.7.0",
"version": "0.7.2",
"source": {
"type": "git",
"url": "https://github.com/alchemy-fr/PHPExiftool.git",
"reference": "7372ca4e43473328bf06bca810558fbad7bb2f95"
"reference": "ba1cb51eceb6562d7996023478977a8739de188b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/7372ca4e43473328bf06bca810558fbad7bb2f95",
"reference": "7372ca4e43473328bf06bca810558fbad7bb2f95",
"url": "https://api.github.com/repos/alchemy-fr/PHPExiftool/zipball/ba1cb51eceb6562d7996023478977a8739de188b",
"reference": "ba1cb51eceb6562d7996023478977a8739de188b",
"shasum": ""
},
"require": {
@@ -452,7 +452,7 @@
"exiftool",
"metadata"
],
"time": "2017-05-18T19:04:04+00:00"
"time": "2019-02-13T13:06:43+00:00"
},
{
"name": "alchemy/queue-bundle",
@@ -1156,6 +1156,48 @@
],
"time": "2016-08-09T20:10:17+00:00"
},
{
"name": "czproject/git-php",
"version": "v3.17.0",
"source": {
"type": "git",
"url": "https://github.com/czproject/git-php.git",
"reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/czproject/git-php/zipball/a7b911b81a2fe626f748a4ac8955353c5777bc6c",
"reference": "a7b911b81a2fe626f748a4ac8955353c5777bc6c",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"nette/tester": "^1.1"
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Jan Pecha",
"email": "janpecha@email.cz"
}
],
"description": "Library for work with Git repository in PHP.",
"keywords": [
"git"
],
"time": "2019-02-09T13:11:36+00:00"
},
{
"name": "dailymotion/sdk",
"version": "1.6.5",

View File

@@ -231,17 +231,19 @@ embed_bundle:
video:
player: videojs
autoplay: false
coverSubdef: previewx4
available-speeds:
cover_subdef: thumbnail
message_start: StartOfMessage
available_speeds:
- 1
- 1.5
- 3
audio:
player: videojs
autoplay: false
cover_subdef: thumbnail
document:
player: flexpaper
enable-pdfjs: true
enable_pdfjs: true
geocoding-providers:
-
map-provider: mapboxWebGL

View File

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

View File

@@ -115,6 +115,7 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormTypeInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Process\ExecutableFinder;
use Unoconv\UnoconvServiceProvider;
use XPDF\PdfToText;
use XPDF\XPDFServiceProvider;
@@ -237,8 +238,19 @@ class Application extends SilexApplication
$this->register(new UnicodeServiceProvider());
$this->register(new ValidatorServiceProvider());
$this->register(new XPDFServiceProvider());
$this->setupXpdf();
if ($this['configuration.store']->isSetup()) {
$binariesConfig = $this['conf']->get(['main', 'binaries']);
$executableFinder = new ExecutableFinder();
$this->register(new XPDFServiceProvider(), [
'xpdf.configuration' => [
'pdftotext.binaries' => isset($binariesConfig['pdftotext_binary']) ? $binariesConfig['pdftotext_binary'] : $executableFinder->find('pdftotext'),
]
]);
$this->setupXpdf();
}
$this->register(new FileServeServiceProvider());
$this->register(new ManipulatorServiceProvider());
$this->register(new PluginServiceProvider());
@@ -653,7 +665,7 @@ class Application extends SilexApplication
private function setupGeonames()
{
$this['geonames.server-uri'] = $this->share(function (Application $app) {
return $app['conf']->get(['registry', 'webservices', 'geonames-server'], 'http://geonames.alchemyasp.com/');
return $app['conf']->get(['registry', 'webservices', 'geonames-server'], 'https://geonames.alchemyasp.com/');
});
}

View File

@@ -51,4 +51,41 @@ abstract class AbstractPluginCommand extends Command
$this->container['plugins.autoloader-generator']->write($manifests);
$output->writeln(" <comment>OK</comment>");
}
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);
$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);
$output->writeln(" <comment>OK</comment>");
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
$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>");
$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);
$output->writeln(" <comment>OK</comment>");
$this->updateConfigFiles($input, $output);
}
}

View File

@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Command\Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\ArrayInput;
class AddPlugin extends AbstractPluginCommand
{
@@ -29,41 +30,36 @@ class AddPlugin extends AbstractPluginCommand
protected function doExecutePluginAction(InputInterface $input, OutputInterface $output)
{
$source = $input->getArgument('source');
$shouldDownload = $this->shouldDownloadPlugin($source);
$temporaryDir = $this->container['temporary-filesystem']->createTemporaryDirectory();
if ($shouldDownload){
$command = $this->getApplication()->find('plugins:download');
$arguments = [
'command' => 'plugins:download',
'source' => $source,
'shouldInstallPlugin' => true
];
$output->write("Importing <info>$source</info>...");
$this->container['plugins.importer']->import($source, $temporaryDir);
$output->writeln(" <comment>OK</comment>");
$downloadInput = new ArrayInput($arguments);
$command->run($downloadInput, $output);
$output->write("Validating plugin...");
$manifest = $this->container['plugins.plugins-validator']->validatePlugin($temporaryDir);
$output->writeln(" <comment>OK</comment> found <info>".$manifest->getName()."</info>");
} else {
$targetDir = $this->container['plugin.path'] . DIRECTORY_SEPARATOR . $manifest->getName();
$output->write("Setting up composer...");
$this->container['plugins.composer-installer']->install($temporaryDir);
$output->writeln(" <comment>OK</comment>");
$output->write("Installing plugin <info>".$manifest->getName()."</info>...");
$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>");
$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);
$output->writeln(" <comment>OK</comment>");
$this->updateConfigFiles($input, $output);
$this->doInstallPlugin($source, $input, $output);
}
return 0;
}
protected function shouldDownloadPlugin($source)
{
$allowedScheme = array('https','ssh');
$scheme = parse_url($source, PHP_URL_SCHEME);
if (in_array($scheme, $allowedScheme)){
return true;
} else{
return false;
}
}
}

View File

@@ -0,0 +1,157 @@
<?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\Plugin;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\ArrayInput;
use Cz\Git\GitRepository as GitRepository;
class DownloadPlugin extends AbstractPluginCommand
{
public function __construct()
{
parent::__construct('plugins:download');
$this
->setDescription('Downloads a plugin to Phraseanet')
->addArgument('source', InputArgument::REQUIRED, 'The source is a remote url (.zip or .git)')
->addArgument('destination', InputArgument::OPTIONAL, 'Download destination')
->addArgument('shouldInstallPlugin', InputArgument::OPTIONAL, 'True or false, determines if plugin should be installed after download');
}
protected function doExecutePluginAction(InputInterface $input, OutputInterface $output)
{
$source = $input->getArgument('source');
$destination = $input->getArgument('destination');
$shouldInstallPlugin = false;
$shouldInstallPlugin = $input->getArgument('shouldInstallPlugin');
$destinationSubdir = '/plugin-'.md5($source);
if ($destination){
$destination = trim($destination);
$destination = rtrim($destination, '/');
$localDownloadPath = $destination;
} else {
$localDownloadPath = '/tmp/plugin-download' . $destinationSubdir;
}
if (!is_dir($localDownloadPath)) {
mkdir($localDownloadPath, 0755, true);
}
$extension = $this->getURIExtension($source);
if ($extension){
switch ($extension){
case 'zip':
$localUnpackPath = '/tmp/plugin-zip'. $destinationSubdir;
if (!is_dir($localUnpackPath)) {
mkdir($localUnpackPath, 0755, true);
}
$localArchiveFile = $localUnpackPath . '/plugin-downloaded.zip';
// download
$output->writeln("Downloading <info>$source</info>...");
set_time_limit(0);
$fp = fopen ($localArchiveFile, 'w+');
$ch = curl_init($source);;
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_exec($ch);
curl_close($ch);
fclose($fp);
// unpack
$output->writeln("Unpacking <info>$source</info>...");
$zip = new \ZipArchive();
$errorUnpack = false;
if ($zip->open($localArchiveFile)) {
for ($i = 0; $i < $zip->numFiles; $i++) {
if (!($zip->extractTo($localDownloadPath, array($zip->getNameIndex($i))))) {
$errorUnpack = true;
}
}
$zip->close();
}
if ($errorUnpack){
$output->writeln("Failed unzipping <info>$source</info>");
} else {
$output->writeln("Plugin downloaded to <info>$localDownloadPath</info>");
if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output);
}
// remove zip archive
$this->delDirTree($localUnpackPath);
break;
case 'git':
$output->writeln("Downloading <info>$source</info>...");
$repo = GitRepository::cloneRepository($source, $localDownloadPath);
$output->writeln("Plugin downloaded to <info>$localDownloadPath</info>");
if ($shouldInstallPlugin) $this->doInstallPlugin($localDownloadPath, $input, $output);
break;
}
} else {
$output->writeln("The source <info>$source</info> is not supported. Only .zip and .git are supported.");
}
return 0;
}
protected function getURIExtension($source)
{
$validExtension = false;
$allowedExtension = array('zip','git');
$path = parse_url($source, PHP_URL_PATH);
if (strpos($path, '.') !== false) {
$pathParts = explode('.', $path);
$extension = $pathParts[1];
if (in_array($extension, $allowedExtension)){
$validExtension = true;
}
}
if ($validExtension){
return $extension;
} else {
return false;
}
}
protected static function delDirTree($dir) {
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? self::delDirTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
}

View File

@@ -944,7 +944,7 @@ class V1Controller extends Controller
}
$originalName = $pi['filename'] . '.' . $pi['extension'];
$newPathname = $tempfile;
$uploadedFilename = $newPathname = $tempfile;
}
}
else {
@@ -956,8 +956,11 @@ class V1Controller extends Controller
if (!$file->isValid()) {
return $this->getBadRequestAction($request, 'Data corrupted, please try again');
}
$uploadedFilename = $file->getPathname();
$originalName = $file->getClientOriginalName();
$newPathname = $file->getPathname() . '.' . $file->getClientOriginalExtension();
if (false === rename($file->getPathname(), $newPathname)) {
return Result::createError($request, 403, 'Error while renaming file')->createResponse();
}
@@ -1010,6 +1013,11 @@ class V1Controller extends Controller
$nosubdef = $request->get('nosubdefs') === '' || \p4field::isyes($request->get('nosubdefs'));
$this->getBorderManager()->process($session, $Package, $callback, $behavior, $nosubdef);
// remove $newPathname on temporary directory
if ($newPathname !== $uploadedFilename) {
@rename($newPathname, $uploadedFilename);
}
$ret = ['entity' => null];
if ($output instanceof \record_adapter) {
@@ -1081,6 +1089,11 @@ class V1Controller extends Controller
}
}
// remove $newPathname on temporary directory
if ($renamedFilename !== $uploadedFilename) {
@rename($renamedFilename, $uploadedFilename);
}
return Result::create($request, $ret)->createResponse();
}
@@ -1984,7 +1997,7 @@ class V1Controller extends Controller
return $this->getBadRequestAction($request);
}
$datas = substr($datas, 0, ($n)) . $value . substr($datas, ($n + 2));
$datas = substr($datas, 0, ($n)) . $value . substr($datas, ($n + 1));
}
$record->setStatus(strrev($datas));
@@ -2588,8 +2601,18 @@ class V1Controller extends Controller
foreach ($recordsData as $data) {
$records[] = $this->addOrDelStoryRecord($story, $data, $action);
if($action === 'ADD' && !$cover_set && isset($data->{'use_as_cover'}) && $data->{'use_as_cover'} === true) {
$coverSource = [];
if (isset($data->{'thumbnail_cover_source'})) {
$coverSource['thumbnail_cover_source'] = $data->{'thumbnail_cover_source'};
}
if (isset($data->{'preview_cover_source'})) {
$coverSource['preview_cover_source'] = $data->{'preview_cover_source'};
}
// because we can try many records as cover source, we let it fail
$cover_set = ($this->setStoryCover($story, $data->{'record_id'}, true) !== false);
$cover_set = ($this->setStoryCover($story, $data->{'record_id'}, true, $coverSource) !== false);
}
}
@@ -2643,14 +2666,26 @@ class V1Controller extends Controller
$story = new \record_adapter($this->app, $databox_id, $story_id);
$coverSource = [];
if (isset($data->{'thumbnail_cover_source'})) {
$coverSource['thumbnail_cover_source'] = $data->{'thumbnail_cover_source'};
}
if (isset($data->{'preview_cover_source'})) {
$coverSource['preview_cover_source'] = $data->{'preview_cover_source'};
}
// we do NOT let "setStoryCover()" fail : pass false as last arg
$record_key = $this->setStoryCover($story, $data->{'record_id'}, false);
$record_key = $this->setStoryCover($story, $data->{'record_id'}, false, $coverSource);
return Result::create($request, array($record_key))->createResponse();
}
protected function setStoryCover(\record_adapter $story, $record_id, $can_fail=false)
protected function setStoryCover(\record_adapter $story, $record_id, $can_fail=false, $coverSource = [])
{
$coverSource = array_merge(['thumbnail_cover_source' => 'thumbnail', 'preview_cover_source' => 'preview'], $coverSource);
try {
$record = new \record_adapter($this->app, $story->getDataboxId(), $record_id);
} catch (\Exception_Record_AdapterNotFound $e) {
@@ -2662,18 +2697,22 @@ class V1Controller extends Controller
$this->app->abort(404, sprintf('Record identified by databox_id %s and record_id %s is not in the story', $story->getDataboxId(), $record_id));
}
if ($record->getType() !== 'image' && $record->getType() !== 'video') {
// this can fail so we can loop on many records during story creation...
if($can_fail) {
return false;
}
$this->app->abort(403, sprintf('Record identified by databox_id %s and record_id %s is not an image nor a video', $story->getDataboxId(), $record_id));
}
// taking account all record type as a cover
// if ($record->getType() !== 'image' && $record->getType() !== 'video') {
// // this can fail so we can loop on many records during story creation...
// if($can_fail) {
// return false;
// }
// $this->app->abort(403, sprintf('Record identified by databox_id %s and record_id %s is not an image nor a video', $story->getDataboxId(), $record_id));
// }
foreach ($record->get_subdefs() as $name => $value) {
if (!in_array($name, array('thumbnail', 'preview'))) {
if (!($key = array_search($name, $coverSource))) {
continue;
}
$name = ($key == 'thumbnail_cover_source') ? 'thumbnail': 'preview';
$media = $this->app->getMediaFromUri($value->getRealPath());
$this->getSubdefSubstituer()->substituteSubdef($story, $name, $media); // name = thumbnail | preview
$this->getDataboxLogger($story->getDatabox())->log(

View File

@@ -114,6 +114,7 @@ class MoveCollectionController extends Controller
$trashCollectionsBySbasId = [];
foreach ($records as $record) {
$oldCollectionId = $record->getCollection()->get_coll_id();
$record->move_to_collection($collection, $this->getApplicationBox());
if ($request->request->get("chg_coll_son") == "1") {
@@ -130,7 +131,7 @@ class MoveCollectionController extends Controller
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
}
if ($trashCollectionsBySbasId[$sbasId] !== null) {
if ($record->getCollection()->get_coll_id() == $trashCollectionsBySbasId[$sbasId]->get_coll_id() && $collection->get_coll_id() !== $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
if ($oldCollectionId == $trashCollectionsBySbasId[$sbasId]->get_coll_id() && $collection->get_coll_id() !== $trashCollectionsBySbasId[$sbasId]->get_coll_id()) {
// record is already in trash so active it
foreach ($record->get_subdefs() as $subdef) {
if (($pl = $subdef->get_permalink())) {

View File

@@ -463,6 +463,8 @@ class PushController extends Controller
}
try {
$manager = $this->getEntityManager();
$password = $this->getRandomGenerator()->generateString(128);
$user = $this->getUserManipulator()->createUser($email, $password, $email);
@@ -476,12 +478,15 @@ class PushController extends Controller
$user->setCompany($request->request->get('company'));
}
if ($request->request->get('job')) {
$user->setCompany($request->request->get('job'));
$user->setJob($request->request->get('job'));
}
if ($request->request->get('form_geonameid')) {
$this->getUserManipulator()->setGeonameId($user, $request->request->get('form_geonameid'));
if ($request->request->get('city')) {
$this->getUserManipulator()->setGeonameId($user, $request->request->get('city'));
}
$manager->persist($user);
$manager->flush();
$result['message'] = $this->app->trans('User successfully created');
$result['success'] = true;
$result['user'] = $this->formatUser($user);

View File

@@ -179,7 +179,6 @@ class QueryController extends Controller
};
$userManipulator->setUserSetting($user, 'last_jsonquery', (string)$request->request->get('jsQuery'));
$jsQuery = @json_decode((string)$request->request->get('jsQuery'), true);
if(($ft = $findFulltext($jsQuery['query'])) !== null) {
$userManipulator->setUserSetting($user, 'start_page_query', $ft);
@@ -215,7 +214,7 @@ class QueryController extends Controller
if (min($d2top, $d2bottom) < 4) {
if ($d2bottom < 4) {
if($page != 1){
$string .= "<a id='PREV_PAGE' class='btn btn-primary btn-mini'></a>";
$string .= "<a id='PREV_PAGE' class='btn btn-primary btn-mini icon-baseline-chevron_left-24px'></a>";
}
for ($i = 1; ($i <= 4 && (($i <= $npages) === true)); $i++) {
if ($i == $page)
@@ -224,13 +223,13 @@ class QueryController extends Controller
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
}
if ($npages > 4)
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini'></a>";
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action" data-page="' . $npages . '" id="last"></a>';
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini icon icon-baseline-chevron_right-24px'></a>";
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action icon icon-double-arrows" data-page="' . $npages . '" id="last"></a>';
} else {
$start = $npages - 4;
if (($start) > 0){
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"></a>';
$string .= '<a id="PREV_PAGE" class="btn btn-primary btn-mini"></a>';
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"><span class="icon icon-double-arrows icon-inverse"></span></a>';
$string .= '<a id="PREV_PAGE" class="btn btn-primary btn-mini icon icon-baseline-chevron_left-24px"></a>';
}else
$start = 1;
for ($i = ($start); $i <= $npages; $i++) {
@@ -240,11 +239,11 @@ class QueryController extends Controller
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
}
if($page < $npages){
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini'></a>";
$string .= "<a id='NEXT_PAGE' class='btn btn-primary btn-mini icon icon-baseline-chevron_right-24px'></a>";
}
}
} else {
$string .= '<a class="btn btn-primary btn-mini btn-mini search-navigate-action" data-page="1" id="first"></a>';
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="1" id="first"><span class="icon icon-double-arrows icon-inverse"></span></a>';
for ($i = ($page - 2); $i <= ($page + 2); $i++) {
if ($i == $page)
@@ -253,10 +252,10 @@ class QueryController extends Controller
$string .= '<a class="btn btn-primary btn-mini search-navigate-action" data-page="'.$i.'">' . $i . '</a>';
}
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action" data-page="' . $npages . '" id="last"></a>';
$string .= '<a href="#" class="btn btn-primary btn-mini search-navigate-action icon icon-double-arrows" data-page="' . $npages . '" id="last"></a>';
}
}
$string .= '<div style="display:none;"><div id="NEXT_PAGE"></div><div id="PREV_PAGE"></div></div>';
$string .= '<div style="display:none;"><div id="NEXT_PAGE" class="icon icon-baseline-chevron_right-24px"></div><div id="PREV_PAGE" class="icon icon-baseline-chevron_left-24px"></div></div>';
$explain = $this->render(
"prod/results/infos.html.twig",
@@ -317,7 +316,7 @@ class QueryController extends Controller
</tfoot>
</table></div></div>'
. '</div><a href="#" class="search-display-info" data-infos="' . str_replace('"', '&quot;', $explain) . '">'
. $this->app->trans('%total% reponses', ['%total%' => '<span>'.$result->getTotal().'</span>']) . '</a>';
. $this->app->trans('%total% reponses', ['%total%' => '<span>'.number_format($result->getTotal(),null, null, ' ').'</span>']) . '</a>';
$json['infos'] = $infoResult;
$json['navigationTpl'] = $string;
@@ -471,7 +470,6 @@ class QueryController extends Controller
$json['results'] = $this->render($template, ['results'=> $result]);
}
return $this->app->json($json);
}

View File

@@ -15,12 +15,11 @@ use Alchemy\Phrasea\Core\Configuration\DisplaySettingService;
use Alchemy\Phrasea\Exception\SessionNotFound;
use Alchemy\Phrasea\Feed\Aggregate;
use Alchemy\Phrasea\Helper;
use Alchemy\Phrasea\Model\Entities\UserSetting;
use Alchemy\Phrasea\Helper\WorkZone as WorkzoneHelper;
use Alchemy\Phrasea\Model\Repositories\FeedRepository;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\HttpFoundation\Request;
class RootController extends Controller
{
use Application\Helper\FirewallAware;
@@ -41,12 +40,11 @@ class RootController extends Controller
public function indexAction(Request $request) {
try {
\Session_Logger::updateClientInfos($this->app, 1);
} catch (SessionNotFound $e) {
}
catch (SessionNotFound $e) {
return $this->app->redirectPath('logout');
}
$css = [];
$user = $this->getAuthenticatedUser();
$cssfile = $this->getSettings()->getUserSetting($user, 'css');
@@ -85,6 +83,22 @@ class RootController extends Controller
/** @var \Closure $filter */
$filter = $this->app['plugin.filter_by_authorization'];
/* prepare work to extend whole taskbar... later
$menus = [
'push' => ['native'=>true, 'n'=>0],
'tools' => ['native'=>true, 'n'=>0],
];
/ ** @var ActionBarPluginInterface $plugin * /
foreach($filter('actionbar') as $kplugin=>$plugin) {
foreach($plugin->getActionBar() as $kmenu=>$menu) {
if(!array_key_exists($kmenu, $menus)) {
$menus[$kmenu] = ['native'=>false, 'n'=>0];
}
$menus[$kmenu]['n']++;
}
}
*/
$plugins = [
'workzone' => $filter('workzone'),
'actionbar' => $filter('actionbar'),
@@ -92,7 +106,7 @@ class RootController extends Controller
return $this->render('prod/index.html.twig', [
'module_name' => 'Production',
'WorkZone' => new Helper\WorkZone($this->app, $request),
'WorkZone' => new WorkzoneHelper($this->app, $request),
'module_prod' => $helper,
'search_datas' => $helper->get_search_datas(),
'cssfile' => $cssfile,
@@ -105,7 +119,7 @@ class RootController extends Controller
'feeds' => $feeds,
'aggregate' => $aggregate,
'GV_google_api' => $conf->get(['registry', 'webservices', 'google-charts-enabled']),
'geocodingProviders' => $conf->get(['geocoding-providers']),
'geocodingProviders' => $conf->get(['geocoding-providers']),
'search_status' => \databox_status::getSearchStatus($this->app),
'thesau_js_list' => $thjslist,
'thesau_json_sbas' => json_encode($sbas),

View File

@@ -367,15 +367,17 @@ class UploadController extends Controller
$postMaxSize = PHP_INT_MAX;
}
$r = 0;
switch (strtolower(substr($postMaxSize, -1))) {
/** @noinspection PhpMissingBreakStatementInspection */
case 'g':
$postMaxSize *= 1024;
$r += 10;
/** @noinspection PhpMissingBreakStatementInspection */
case 'm':
$postMaxSize *= 1024;
$r += 10;
case 'k':
$postMaxSize *= 1024;
$r += 10;
$postMaxSize = ((int)($postMaxSize))<<$r;
}
return min(UploadedFile::getMaxFilesize(), (int) $postMaxSize);

View File

@@ -522,27 +522,35 @@ class AccountController extends Controller
$list = array_keys($this->app['repo.collections-registry']->getBaseIdMap());
$this->app->getAclForUser($user)->revoke_access_from_bases($list);
try {
$this->app->getAclForUser($user)->revoke_access_from_bases($list);
}
catch (\Exception $e) {
// one or more access could not be revoked ? the user will not be phantom
$this->app->addFlash('error', $this->app->trans('phraseanet::error: failed to revoke some user access'));
}
if ($this->app->getAclForUser($user)->is_phantom()) {
// send confirmation email: the account has been deleted
try {
$receiver = Receiver::fromUser($user);
} catch (InvalidArgumentException $e) {
$this->app->addFlash('error', $this->app->trans('phraseanet::erreur: echec du serveur de mail'));
$mail = MailSuccessAccountDelete::create($this->app, $receiver);
}
catch (InvalidArgumentException $e) {
$this->app->addFlash('error', $this->app->trans('phraseanet::erreur: echec du serveur de mail'));
$mail = null;
}
$mail = MailSuccessAccountDelete::create($this->app, $receiver);
$this->app['manipulator.user']->delete($user);
$this->deliver($mail);
if($mail) {
$this->deliver($mail);
}
$this->getAuthenticator()->closeAccount();
$this->app->addFlash('info', $this->app->trans('phraseanet::account The account has been deleted'));
}
$this->getAuthenticator()->closeAccount();
$this->app->addFlash('info', $this->app->trans('phraseanet::account The account has been deleted'));
}
/**

View File

@@ -265,7 +265,7 @@ class LoginController extends Controller
return $this->render('login/register-classic.html.twig', array_merge(
$this->getDefaultTemplateVariables($request),
[
'geonames_server_uri' => str_replace(sprintf('%s:', parse_url($url, PHP_URL_SCHEME)), '', $url),
'geonames_server_uri' => $url,
'form' => $form->createView()
]));
}

View File

@@ -94,10 +94,9 @@ class SessionController extends Controller
}
/**
* Check session state
*
* @param Request $request
* @param Request $request
* @return JsonResponse
* @throws \Exception in case "new \DateTime()" fails ?
*/
public function updateSession(Request $request)
{
@@ -120,7 +119,8 @@ class SessionController extends Controller
return $this->app->json($ret);
}
} else {
}
else {
$ret['status'] = 'disconnected';
return $this->app->json($ret);
@@ -128,7 +128,8 @@ class SessionController extends Controller
try {
$this->getApplicationBox()->get_connection();
} catch (\Exception $e) {
}
catch (\Exception $e) {
return $this->app->json($ret);
}
@@ -148,8 +149,9 @@ class SessionController extends Controller
$module->setModuleId($moduleId);
$module->setSession($session);
$manager->persist($module);
} else {
$manager->persist($session->getModuleById($moduleId)->setUpdated(new \DateTime()));
}
else {
$manager->persist($session->getModuleById($moduleId)->setUpdated($now));
}
$manager->persist($session);
@@ -231,7 +233,10 @@ class SessionController extends Controller
*/
private function getBasketRepository()
{
return $this->getEntityManager()->getRepository('Phraseanet:Basket');
/** @var BasketRepository $ret */
$ret = $this->getEntityManager()->getRepository('Phraseanet:Basket');
return $ret;
}
/**

View File

@@ -146,7 +146,7 @@ class RegistryFormManipulator
],
'webservices' => [
'google-charts-enabled' => true,
'geonames-server' => 'http://geonames.alchemyasp.com/',
'geonames-server' => 'https://geonames.alchemyasp.com/',
'captchas-enabled' => false,
'recaptcha-public-key' => '',
'recaptcha-private-key' => '',

View File

@@ -16,7 +16,7 @@ class Version
/**
* @var string
*/
private $number = '4.1.0-alpha.14a';
private $number = '4.1.0-alpha.15a';
/**
* @var string

View File

@@ -11,28 +11,29 @@
namespace Alchemy\Phrasea\Helper;
use Doctrine\Common\Collections\ArrayCollection;
use Alchemy\Phrasea\Model\Entities\Basket as BasketEntity;
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
use Doctrine\Common\Collections\ArrayCollection;
class WorkZone extends Helper
{
const BASKETS = 'baskets';
const STORIES = 'stories';
const BASKETS = 'baskets';
const STORIES = 'stories';
const VALIDATIONS = 'validations';
/**
*
* Returns an ArrayCollection containing three keys :
* - self::BASKETS : an ArrayCollection of the actives baskets
* (Non Archived)
* - self::BASKETS : an ArrayCollection of the actives baskets (Non Archived)
* - self::STORIES : an ArrayCollection of working stories
* - self::VALIDATIONS : the validation people are waiting from me
*
* @return \Doctrine\Common\Collections\ArrayCollection
* @param null|string $sort "date"|"name"
* @return ArrayCollection
*/
public function getContent($sort)
public function getContent($sort = null)
{
/* @var $repo_baskets Alchemy\Phrasea\Model\Repositories\BasketRepository */
/* @var $repo_baskets BasketRepository */
$repo_baskets = $this->app['repo.baskets'];
$sort = in_array($sort, ['date', 'name']) ? $sort : 'name';
@@ -42,7 +43,7 @@ class WorkZone extends Helper
$baskets = $repo_baskets->findActiveByUser($this->app->getAuthenticatedUser(), $sort);
// force creation of a default basket
if (0 === count($baskets)) {
if (count($baskets) === 0) {
$basket = new BasketEntity();
$basket->setName($this->app->trans('Default basket'));
@@ -55,7 +56,7 @@ class WorkZone extends Helper
$validations = $repo_baskets->findActiveValidationByUser($this->app->getAuthenticatedUser(), $sort);
/* @var $repo_stories Alchemy\Phrasea\Model\Repositories\StoryWZRepository */
/* @var $repo_stories StoryWZRepository */
$repo_stories = $this->app['repo.story-wz'];
$stories = $repo_stories->findByUser($this->app, $this->app->getAuthenticatedUser(), $sort);

View File

@@ -61,7 +61,7 @@ class SubdefGenerator
public function generateSubdefs(\record_adapter $record, array $wanted_subdefs = null)
{
if ($record->get_hd_file() !== null) {
if ($record->get_hd_file() !== null && $record->get_hd_file()->getMimeType() == "application/x-indesign") {
$mediaSource = $this->mediavorus->guess($record->get_hd_file()->getPathname());
$metadatas = $mediaSource->getMetadatas();
@@ -69,15 +69,27 @@ class SubdefGenerator
if(!isset($this->tmpFilesystem)){
$this->tmpFilesystem = Manager::create();
}
$tmpDir = $this->tmpFilesystem->createTemporaryDirectory();
$tmpDir = $this->tmpFilesystem->createTemporaryDirectory(0777, 500);
try {
$this->app['filesystem']->dumpFile($tmpDir.'/file.jpg', $metadatas->get('XMP-xmp:PageImage')->getValue()->asString());
$this->tmpFilePath = $tmpDir.'/file.jpg';
} catch (\Exception $e) {
$this->logger->error(sprintf('Unable to write temporary file : %s', $e->getMessage()));
$files = $this->app['exiftool.preview-extractor']->extract($record->get_hd_file()->getPathname(), $tmpDir);
$selected = null;
$size = null;
foreach ($files as $file) {
if ($file->isDir() || $file->isDot()) {
continue;
}
if (is_null($selected) || $file->getSize() > $size) {
$selected = $file->getPathname();
$size = $file->getSize();
}
}
if ($selected) {
$this->tmpFilePath = $selected;
}
}
}

View File

@@ -73,6 +73,9 @@ class SubdefSubstituer
$this->createMediaSubdef($record, 'document', $media);
$record->setMimeType($media->getFile()->getMimeType());
$record->setType($media->getType());
$record->write_metas();
if ($shouldSubdefsBeRebuilt) {

View File

@@ -14,6 +14,7 @@ namespace Alchemy\Phrasea\Metadata;
use Alchemy\Phrasea\Border\File;
use Alchemy\Phrasea\Databox\DataboxRepository;
use Alchemy\Phrasea\Metadata\Tag\NoSource;
use DateTime;
use PHPExiftool\Driver\Metadata\Metadata;
class PhraseanetMetadataSetter
@@ -66,8 +67,16 @@ class PhraseanetMetadataSetter
continue;
}
$data['value'] = $value;
if ($field->get_type() == 'date') {
try {
$dateTime = new DateTime($value);
$value = $dateTime->format('Y/m/d H:i:s');
} catch (\Exception $e) {
// $value unchanged
}
}
$data['value'] = $value;
$metadataInRecordFormat[] = $data;
}
}

View File

@@ -54,21 +54,24 @@ class BasketRepository extends EntityRepository
/**
* Returns all basket for a given user that are not marked as archived
*
* @param User $user
* @param User $user
* @param null|string $sort
* @return Basket[]
*/
public function findActiveByUser(User $user, $sort = null)
{
$dql = 'SELECT b
FROM Phraseanet:Basket b
LEFT JOIN b.elements e
WHERE b.user = :usr_id
AND b.archived = false';
// checked : 4 usages, "b.elements" is useless
$dql = "SELECT b\n"
. " FROM Phraseanet:Basket b\n"
// . " LEFT JOIN b.elements e\n" //
. " WHERE b.user = :usr_id\n"
. " AND b.archived = false";
if ($sort == 'date') {
$dql .= ' ORDER BY b.created DESC';
} elseif ($sort == 'name') {
$dql .= ' ORDER BY b.name ASC';
$dql .= "\n ORDER BY b.created DESC";
}
elseif ($sort == 'name') {
$dql .= "\n ORDER BY b.name ASC";
}
$query = $this->_em->createQuery($dql);
@@ -80,24 +83,27 @@ class BasketRepository extends EntityRepository
/**
* Returns all unread basket for a given user that are not marked as archived
*
* @param User $user
* @param User $user
* @return Basket[]
*/
public function findUnreadActiveByUser(User $user)
{
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
LEFT JOIN b.validation s
LEFT JOIN s.participants p
WHERE b.archived = false
AND (
(b.user = :usr_id_owner AND b.isRead = false)
OR (b.user != :usr_id_ownertwo
AND p.user = :usr_id_participant
AND p.is_aware = false)
)
AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP())';
// checked : 2 usages, "b.elements" is useless
$dql = "SELECT b\n"
. " FROM Phraseanet:Basket b\n"
// . " JOIN b.elements e\n"
. " LEFT JOIN b.validation s\n"
. " LEFT JOIN s.participants p\n"
. " WHERE b.archived = false\n"
. " AND (\n"
. " (b.user = :usr_id_owner AND b.isRead = false)\n"
. " OR \n"
. " (b.user != :usr_id_ownertwo\n"
. " AND p.user = :usr_id_participant\n"
. " AND p.is_aware = false\n"
. " AND s.expires > CURRENT_TIMESTAMP()\n"
. " )\n"
. " )";
$params = [
'usr_id_owner' => $user->getId(),
@@ -115,11 +121,22 @@ class BasketRepository extends EntityRepository
* Returns all baskets that are in validation session not expired and
* where a specified user is participant (not owner)
*
* @param User $user
* @param User $user
* @param null|string $sort
* @return Basket[]
*/
public function findActiveValidationByUser(User $user, $sort = null)
{
// checked : 2 usages, "b.elements" seems useless.
$dql = "SELECT b\n"
. "FROM Phraseanet:Basket b\n"
// . " JOIN b.elements e\n"
// . " JOIN e.validation_datas v\n"
. " JOIN b.validation s\n"
. " JOIN s.participants p\n"
. "WHERE b.user != ?1 AND p.user = ?2\n"
. " AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP())";
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
@@ -130,9 +147,9 @@ class BasketRepository extends EntityRepository
AND (s.expires IS NULL OR s.expires > CURRENT_TIMESTAMP()) ';
if ($sort == 'date') {
$dql .= ' ORDER BY b.created DESC';
$dql .= "\nORDER BY b.created DESC";
} elseif ($sort == 'name') {
$dql .= ' ORDER BY b.name ASC';
$dql .= "\nORDER BY b.name ASC";
}
$query = $this->_em->createQuery($dql);
@@ -152,10 +169,11 @@ class BasketRepository extends EntityRepository
*/
public function findUserBasket($basket_id, User $user, $requireOwner)
{
$dql = 'SELECT b
FROM Phraseanet:Basket b
LEFT JOIN b.elements e
WHERE b.id = :basket_id';
// checked : 3 usages, "b.elements e" seems useless
$dql = "SELECT b\n"
. " FROM Phraseanet:Basket b\n"
// . " LEFT JOIN b.elements e\n"
. " WHERE b.id = :basket_id";
$query = $this->_em->createQuery($dql);
$query->setParameters(['basket_id' => $basket_id]);
@@ -188,7 +206,7 @@ class BasketRepository extends EntityRepository
public function findContainingRecordForUser(\record_adapter $record, User $user)
{
// todo : check "e.sbas_id = e.sbas_id" ???
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
@@ -210,30 +228,31 @@ class BasketRepository extends EntityRepository
{
switch ($type) {
case self::RECEIVED:
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
WHERE b.user = :usr_id AND b.pusher_id IS NOT NULL';
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
$dql = "SELECT b\n"
. "FROM Phraseanet:Basket b\n"
. " JOIN b.elements e\n"
. "WHERE b.user = :usr_id AND b.pusher_id IS NOT NULL";
$params = [
'usr_id' => $user->getId()
];
break;
case self::VALIDATION_DONE:
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
JOIN b.validation s
JOIN s.participants p
WHERE b.user != ?1 AND p.user = ?2';
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
$dql = "SELECT b\n"
. "FROM Phraseanet:Basket b\n"
. " JOIN b.elements e\n"
. " JOIN b.validation s\n"
. " JOIN s.participants p\n"
. "WHERE b.user != ?1 AND p.user = ?2";
$params = [
1 => $user->getId()
, 2 => $user->getId()
1 => $user->getId(),
2 => $user->getId()
];
break;
case self::VALIDATION_SENT:
$dql = 'SELECT b
FROM Phraseanet:Basket b
JOIN b.elements e
JOIN b.validation v
WHERE b.user = :usr_id';
$params = [
@@ -243,7 +262,6 @@ class BasketRepository extends EntityRepository
case self::MYBASKETS:
$dql = 'SELECT b
FROM Phraseanet:Basket b
LEFT JOIN b.elements e
LEFT JOIN b.validation s
LEFT JOIN s.participants p
WHERE (b.user = :usr_id)';
@@ -252,6 +270,7 @@ class BasketRepository extends EntityRepository
];
break;
default:
// todo : check when called, and if "LEFT JOIN b.elements e" is usefull
$dql = 'SELECT b
FROM Phraseanet:Basket b
LEFT JOIN b.elements e
@@ -297,6 +316,7 @@ class BasketRepository extends EntityRepository
*/
public function findActiveValidationAndBasketByUser(User $user, $sort = null)
{
// todo : check caller and if "LEFT JOIN b.elements e" is usefull
$dql = 'SELECT b
FROM Phraseanet:Basket b
LEFT JOIN b.elements e

View File

@@ -30,6 +30,11 @@ class FieldKey implements Key, QueryPostProcessor
return $this->getField($context)->getIndexField($raw);
}
public function getFieldType(QueryContext $context)
{
return $this->getField($context)->getType();
}
public function isValueCompatible($value, QueryContext $context)
{
return ValueChecker::isValueCompatible($this->getField($context), $value);

View File

@@ -6,6 +6,7 @@ use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
interface Key
{
public function getFieldType(QueryContext $context);
public function getIndexField(QueryContext $context, $raw = false);
public function isValueCompatible($value, QueryContext $context);
public function __toString();

View File

@@ -23,6 +23,11 @@ class MetadataKey implements Key
return $this->getTag($context)->getIndexField($raw);
}
public function getFieldType(QueryContext $context)
{
return $this->getTag($context)->getType();
}
public function isValueCompatible($value, QueryContext $context)
{
return ValueChecker::isValueCompatible($this->getTag($context), $value);

View File

@@ -52,6 +52,11 @@ class NativeKey implements Key
$this->key = $key;
}
public function getFieldType(QueryContext $context)
{
return $this->type;
}
public function getIndexField(QueryContext $context, $raw = false)
{
return $this->key;

View File

@@ -2,18 +2,20 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue;
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Field as StructureField;
use Assert\Assertion;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\FieldKey;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\KeyValue\Key;
use Alchemy\Phrasea\SearchEngine\Elastic\AST\Node;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\QueryException;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryContext;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryHelper;
use Alchemy\Phrasea\SearchEngine\Elastic\Search\QueryPostProcessor;
class RangeExpression extends Node
{
/** @var FieldKey */
private $key;
private $lower_bound;
private $lower_inclusive;
private $higher_bound;
@@ -55,20 +57,34 @@ class RangeExpression extends Node
public function buildQuery(QueryContext $context)
{
$params = array();
if ($this->lower_bound !== null) {
$this->assertValueCompatible($this->lower_bound, $context);
if ($this->lower_inclusive) {
$params['gte'] = $this->lower_bound;
} else {
$params['gt'] = $this->lower_bound;
/** @var StructureField $field */
// $field = $this->key->getField($context);
$lower_bound = $this->lower_bound;
$higher_bound = $this->higher_bound;
if($this->key->getFieldType($context) === FieldMapping::TYPE_DATE) {
if($lower_bound !== null) {
$lower_bound = RecordHelper::sanitizeDate($lower_bound);
}
if($higher_bound !== null) {
$higher_bound = RecordHelper::sanitizeDate($higher_bound);
}
}
if ($this->higher_bound !== null) {
$this->assertValueCompatible($this->higher_bound, $context);
if ($this->higher_inclusive) {
$params['lte'] = $this->higher_bound;
if ($lower_bound !== null) {
$this->assertValueCompatible($lower_bound, $context);
if ($this->lower_inclusive) {
$params['gte'] = $lower_bound;
} else {
$params['lt'] = $this->higher_bound;
$params['gt'] = $lower_bound;
}
}
if ($higher_bound !== null) {
$this->assertValueCompatible($higher_bound, $context);
if ($this->higher_inclusive) {
$params['lte'] = $higher_bound;
} else {
$params['lt'] = $higher_bound;
}
}

View File

@@ -34,6 +34,11 @@ class TimestampKey implements Key, Typed
return FieldMapping::TYPE_DATE;
}
public function getFieldType(QueryContext $context)
{
return FieldMapping::TYPE_DATE;
}
public function getIndexField(QueryContext $context, $raw = false)
{
return $this->index_field;

View File

@@ -396,10 +396,10 @@ class ElasticSearchEngine implements SearchEngineInterface
if ($options->getDateFields() && ($options->getMaxDate() || $options->getMinDate())) {
$range = [];
if ($options->getMaxDate()) {
$range['lte'] = $options->getMaxDate()->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
$range['lte'] = $options->getMaxDate()->format('Y-m-d');
}
if ($options->getMinDate()) {
$range['gte'] = $options->getMinDate()->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
$range['gte'] = $options->getMinDate()->format('Y-m-d');
}
foreach ($options->getDateFields() as $dateField) {

View File

@@ -16,8 +16,7 @@ class FieldMapping
const DATE_FORMAT_MYSQL = 'yyyy-MM-dd HH:mm:ss';
const DATE_FORMAT_CAPTION = 'yyyy/MM/dd'; // ES format
const DATE_FORMAT_MYSQL_OR_CAPTION = 'yyyy-MM-dd HH:mm:ss||yyyy/MM/dd';
const DATE_FORMAT_CAPTION_PHP = 'Y/m/d'; // PHP format
const DATE_FORMAT_MYSQL_OR_CAPTION = 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||yyyy-MM||yyyy';
// Core types
const TYPE_STRING = 'string';

View File

@@ -155,15 +155,16 @@ class BulkOperation
// nb: results (items) are returned IN THE SAME ORDER as commands were pushed in the stack
// so the items[X] match the operationIdentifiers[X]
foreach ($response['items'] as $key => $item) {
foreach($item as $command=>$result) { // command may be "index" or "delete"
if($response['errors'] && $result['status'] >= 400) { // 4xx or 5xx error
throw new Exception(sprintf('%d: %s', $key, var_export($result, true)));
foreach ($item as $command=>$result) { // command may be "index" or "delete"
if ($response['errors'] && $result['status'] >= 400) { // 4xx or 5xx
$err = array_key_exists('error', $result) ? var_export($result['error'], true) : ($command . " error " . $result['status']);
throw new Exception(sprintf('%d: %s', $key, $err));
}
}
$operationIdentifier = $this->operationIdentifiers[$key];
if(is_string($operationIdentifier) || is_int($operationIdentifier)) { // dont include null keys
if (is_string($operationIdentifier) || is_int($operationIdentifier)) { // dont include null keys
$callbackData[$operationIdentifier] = $response['items'][$key];
}
}

View File

@@ -39,18 +39,13 @@ class MetadataHydrator implements HydratorInterface
public function hydrateRecords(array &$records)
{
$sql = <<<SQL
(SELECT record_id, ms.name AS `key`, m.value AS value, 'caption' AS type, ms.business AS private
FROM metadatas AS m
INNER JOIN metadatas_structure AS ms ON (ms.id = m.meta_struct_id)
WHERE record_id IN (?))
UNION
(SELECT record_id, t.name AS `key`, t.value AS value, 'exif' AS type, 0 AS private
FROM technical_datas AS t
WHERE record_id IN (?))
SQL;
$sql = "(SELECT record_id, ms.name AS `key`, m.value AS value, 'caption' AS type, ms.business AS private\n"
. " FROM metadatas AS m INNER JOIN metadatas_structure AS ms ON (ms.id = m.meta_struct_id)\n"
. " WHERE record_id IN (?))\n"
. "UNION\n"
. "(SELECT record_id, t.name AS `key`, t.value AS value, 'exif' AS type, 0 AS private\n"
. " FROM technical_datas AS t\n"
. " WHERE record_id IN (?))\n";
$ids = array_keys($records);
$statement = $this->connection->executeQuery(
@@ -62,7 +57,7 @@ SQL;
while ($metadata = $statement->fetch()) {
// Store metadata value
$key = $metadata['key'];
$value = $metadata['value'];
$value = trim($metadata['value']);
// Do not keep empty values
if ($key === '' || $value === '') {
@@ -80,7 +75,7 @@ SQL;
case 'caption':
// Sanitize fields
$value = StringHelper::crlfNormalize($value);
$value = $this->sanitizeValue($value, $this->structure->typeOf($key));
$value = $this->helper->sanitizeValue($value, $this->structure->typeOf($key));
// Private caption fields are kept apart
$type = $metadata['private'] ? 'private_caption' : 'caption';
// Caption are multi-valued
@@ -103,7 +98,7 @@ SQL;
}
$tag = $this->structure->getMetadataTagByName($key);
if ($tag) {
$value = $this->sanitizeValue($value, $tag->getType());
$value = $this->helper->sanitizeValue($value, $tag->getType());
}
// EXIF data is single-valued
$record['metadata_tags'][$key] = $value;
@@ -118,33 +113,6 @@ SQL;
$this->clearGpsPositionBuffer();
}
private function sanitizeValue($value, $type)
{
switch ($type) {
case FieldMapping::TYPE_STRING:
return str_replace("\0", "", $value);
case FieldMapping::TYPE_DATE:
return $this->helper->sanitizeDate($value);
case FieldMapping::TYPE_FLOAT:
case FieldMapping::TYPE_DOUBLE:
return (float) $value;
case FieldMapping::TYPE_INTEGER:
case FieldMapping::TYPE_LONG:
case FieldMapping::TYPE_SHORT:
case FieldMapping::TYPE_BYTE:
return (int) $value;
case FieldMapping::TYPE_BOOLEAN:
return (bool) $value;
default:
return $value;
}
}
private function handleGpsPosition(&$records, $id, $tag_name, $value)
{
// Get position object

View File

@@ -11,6 +11,8 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\Indexer\Record\Hydrator;
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\Connection as DriverConnection;
@@ -18,31 +20,34 @@ class TitleHydrator implements HydratorInterface
{
private $connection;
public function __construct(DriverConnection $connection)
/** @var RecordHelper */
private $helper;
public function __construct(DriverConnection $connection, RecordHelper $helper)
{
$this->connection = $connection;
$this->helper = $helper;
}
public function hydrateRecords(array &$records)
{
$sql = <<<SQL
SELECT
m.`record_id`,
CASE ms.`thumbtitle`
WHEN "1" THEN "default"
WHEN "0" THEN "default"
ELSE ms.`thumbtitle`
END AS locale,
CASE ms.`thumbtitle`
WHEN "0" THEN r.`originalname`
ELSE GROUP_CONCAT(m.`value` ORDER BY ms.`thumbtitle`, ms.`sorter` SEPARATOR " - ")
END AS title
FROM metadatas AS m FORCE INDEX(`record_id`)
STRAIGHT_JOIN metadatas_structure AS ms ON (ms.`id` = m.`meta_struct_id`)
STRAIGHT_JOIN record AS r ON (r.`record_id` = m.`record_id`)
WHERE m.`record_id` IN (?)
GROUP BY m.`record_id`, ms.`thumbtitle`
SQL;
$sql = "SELECT\n"
. "m.`record_id`,\n"
. " CASE ms.`thumbtitle`\n"
. " WHEN '1' THEN 'default'\n"
. " WHEN '0' THEN 'default'\n"
. " ELSE ms.`thumbtitle`\n"
. " END AS locale,\n"
. " CASE ms.`thumbtitle`\n"
. " WHEN '0' THEN r.`originalname`\n"
. " ELSE GROUP_CONCAT(m.`value` ORDER BY ms.`thumbtitle`, ms.`sorter` SEPARATOR ' - ')\n"
. " END AS title\n"
. "FROM metadatas AS m FORCE INDEX(`record_id`)\n"
. "STRAIGHT_JOIN metadatas_structure AS ms ON (ms.`id` = m.`meta_struct_id`)\n"
. "STRAIGHT_JOIN record AS r ON (r.`record_id` = m.`record_id`)\n"
. "WHERE m.`record_id` IN (?)\n"
. "GROUP BY m.`record_id`, ms.`thumbtitle`\n";
$statement = $this->connection->executeQuery(
$sql,
array(array_keys($records)),
@@ -50,7 +55,7 @@ SQL;
);
while ($row = $statement->fetch()) {
$records[$row['record_id']]['title'][$row['locale']] = $row['title'];
$records[$row['record_id']]['title'][$row['locale']] = $this->helper->sanitizeValue($row['title'], FieldMapping::TYPE_STRING);
}
}
}

View File

@@ -57,6 +57,9 @@ class DateFieldMapping extends ComplexFieldMapping
*/
protected function getProperties()
{
return array_merge([ 'format' => $this->format ], parent::getProperties());
return array_merge([
'format' => $this->format,
'ignore_malformed' => true
], parent::getProperties());
}
}

View File

@@ -89,31 +89,72 @@ class RecordHelper
return $this->collectionMap;
}
/**
* @param string $date
* @return bool
*/
public static function validateDate($date)
{
$d = DateTime::createFromFormat(FieldMapping::DATE_FORMAT_CAPTION_PHP, $date);
return $d && $d->format(FieldMapping::DATE_FORMAT_CAPTION_PHP) == $date;
}
/**
* @param string $value
* @return null|string
*/
public static function sanitizeDate($value)
{
// introduced in https://github.com/alchemy-fr/Phraseanet/commit/775ce804e0257d3a06e4e068bd17330a79eb8370#diff-bee690ed259e0cf73a31dee5295d2edcR286
// not sure if it's really needed
$v_fix = null;
try {
$date = new \DateTime($value);
return $date->format(FieldMapping::DATE_FORMAT_CAPTION_PHP);
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
switch (count($a)) {
case 1: // yyyy
$date = new \DateTime($a[0] . '-01-01'); // will throw if date is not valid
$v_fix = $date->format('Y');
break;
case 2: // yyyy;mm
$date = new \DateTime( $a[0] . '-' . $a[1] . '-01');
$v_fix = $date->format('Y-m');
break;
case 3: // yyyy;mm;dd
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2]);
$v_fix = $date->format('Y-m-d');
break;
case 4:
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':00:00');
$v_fix = $date->format('Y-m-d H:i:s');
break;
case 5:
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':00');
$v_fix = $date->format('Y-m-d H:i:s');
break;
case 6:
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]);
$v_fix = $date->format('Y-m-d H:i:s');
break;
}
} catch (\Exception $e) {
return null;
// no-op, v_fix = null
}
return $v_fix;
}
public function sanitizeValue($value, $type)
{
switch ($type) {
case FieldMapping::TYPE_DATE:
return self::sanitizeDate($value);
case FieldMapping::TYPE_FLOAT:
case FieldMapping::TYPE_DOUBLE:
return (float) $value;
case FieldMapping::TYPE_INTEGER:
case FieldMapping::TYPE_LONG:
case FieldMapping::TYPE_SHORT:
case FieldMapping::TYPE_BYTE:
return (int) $value;
case FieldMapping::TYPE_BOOLEAN:
return (bool) $value;
case FieldMapping::TYPE_STRING:
return str_replace("\0", '', $value);
default:
return $value;
}
}
}

View File

@@ -110,41 +110,50 @@ class QueryHelper
}
}
public static function getRangeFromDateString($string)
public static function getRangeFromDateString($value)
{
$formats = ['Y/m/d', 'Y/m', 'Y'];
$deltas = ['+1 day', '+1 month', '+1 year'];
$to = null;
while ($format = array_pop($formats)) {
$delta = array_pop($deltas);
$from = date_create_from_format($format, $string);
if ($from !== false) {
// Rewind to start of range
$month = 1;
$day = 1;
switch ($format) {
case 'Y/m/d':
$day = (int) $from->format('d');
case 'Y/m':
$month = (int) $from->format('m');
case 'Y':
$year = (int) $from->format('Y');
}
date_date_set($from, $year, $month, $day);
date_time_set($from, 0, 0, 0);
// Create end of the the range
$to = date_modify(clone $from, $delta);
break;
$date_from = null;
$date_to = null;
try {
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
switch (count($a)) {
case 1: // yyyy
$date_to = clone($date_from = new \DateTime($a[0] . '-01-01 00:00:00')); // will throw if date is not valid
$date_to->add(new \DateInterval('P1Y'));
break;
case 2: // yyyy;mm
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-01 00:00:00')); // will throw if date is not valid
$date_to->add(new \DateInterval('P1M'));
break;
case 3: // yyyy;mm;dd
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' 00:00:00')); // will throw if date is not valid
$date_to->add(new \DateInterval('P1D'));
break;
case 4:
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':00:00'));
$date_to->add(new \DateInterval('PT1H'));
break;
case 5:
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':00'));
$date_to->add(new \DateInterval('PT1M'));
break;
case 6:
$date_to = clone($date_from = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]));
// $date_to->add(new \DateInterval('PT1S')); // no need since precision is 1 sec, a "equal" will be generated when from==to
break;
}
}
catch (\Exception $e) {
// no-op
}
if (!$from || !$to) {
throw new \InvalidArgumentException(sprintf('Invalid date "%s".', $string));
if ($date_from === null || $date_to === null) {
throw new \InvalidArgumentException(sprintf('Invalid date "%s".', $value));
}
return [
'from' => $from->format(FieldMapping::DATE_FORMAT_CAPTION_PHP),
'to' => $to->format(FieldMapping::DATE_FORMAT_CAPTION_PHP)
'from' => $date_from->format('Y-m-d H:i:s'),
'to' => $date_to->format('Y-m-d H:i:s')
];
}
}

View File

@@ -5,7 +5,7 @@ namespace Alchemy\Phrasea\SearchEngine\Elastic\Search;
use Alchemy\Phrasea\SearchEngine\Elastic\AST;
use Alchemy\Phrasea\SearchEngine\Elastic\Exception\Exception;
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Alchemy\Phrasea\SearchEngine\Elastic\Structure\Structure;
use Hoa\Compiler\Llk\TreeNode;
use Hoa\Visitor\Element;
@@ -166,6 +166,12 @@ class QueryVisitor implements Visit
$key = $node->getChild(0)->accept($this);
$boundary = $node->getChild(1)->accept($this);
if ($this->isDateKey($key)) {
if(($v = RecordHelper::sanitizeDate($boundary)) !== null) {
$boundary = $v;
}
}
switch ($node->getId()) {
case NodeTypes::LT_EXPR:
return AST\KeyValue\RangeExpression::lessThan($key, $boundary);
@@ -195,11 +201,15 @@ class QueryVisitor implements Visit
try {
// Try to create a range for incomplete dates
$range = QueryHelper::getRangeFromDateString($right);
return new AST\KeyValue\RangeExpression(
$left,
$range['from'], true,
$range['to'], false
);
if ($range['from'] === $range['to']) {
return new AST\KeyValue\EqualExpression($left, $range['from']);
} else {
return new AST\KeyValue\RangeExpression(
$left,
$range['from'], true,
$range['to'], false
);
}
} catch (\InvalidArgumentException $e) {
// Fall back to equal expression
}

View File

@@ -3,7 +3,6 @@
namespace Alchemy\Phrasea\SearchEngine\Elastic\Structure;
use Alchemy\Phrasea\SearchEngine\Elastic\FieldMapping;
use Alchemy\Phrasea\SearchEngine\Elastic\Mapping;
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
use Assert\Assertion;
@@ -20,7 +19,7 @@ class ValueChecker
{
Assertion::allIsInstanceOf($list, Typed::class);
$is_numeric = is_numeric($value);
$is_valid_date = RecordHelper::validateDate($value);
$is_valid_date = (RecordHelper::sanitizeDate($value) !== null);
$filtered = [];
foreach ($list as $item) {
switch ($item->getType()) {

View File

@@ -121,13 +121,16 @@ class WriteMetadataJob extends AbstractJob
$fieldName = $fieldStructure->get_name();
// skip fields with no src
if($tagName == '') {
if($tagName == '' || $tagName == 'Phraseanet:no-source') {
continue;
}
// check exiftool known tags to skip Phraseanet:tf-*
try {
TagFactory::getFromRDFTagname($tagName);
$tag = TagFactory::getFromRDFTagname($tagName);
if(!$tag->isWritable()) {
continue;
}
} catch (TagUnknown $e) {
continue;
}
@@ -139,29 +142,42 @@ class WriteMetadataJob extends AbstractJob
if ($fieldStructure->is_multi()) {
$values = array();
foreach ($fieldValues as $value) {
$values[] = $value->getValue();
$values[] = $this->removeNulChar($value->getValue());
}
$value = new Value\Multi($values);
} else {
$fieldValue = array_pop($fieldValues);
$value = $fieldValue->getValue();
$value = $this->removeNulChar($fieldValue->getValue());
$value = new Value\Mono($value);
// fix the dates edited into phraseanet
if($fieldStructure->get_type() === $fieldStructure::TYPE_DATE) {
try {
$value = self::fixDate($value); // will return NULL if the date is not valid
}
catch (\Exception $e) {
$value = null; // do NOT write back to iptc
}
}
if($value !== null) { // do not write invalid dates
$value = new Value\Mono($value);
}
}
} catch(\Exception $e) {
} catch (\Exception $e) {
// the field is not set in the record, erase it
if ($fieldStructure->is_multi()) {
$value = new Value\Multi(array(''));
}
else {
} else {
$value = new Value\Mono('');
}
}
$metadata->add(
new Metadata\Metadata($fieldStructure->get_tag(), $value)
);
if($value !== null) { // do not write invalid data
$metadata->add(
new Metadata\Metadata($fieldStructure->get_tag(), $value)
);
}
}
$writer = $this->getMetadataWriter($jobData->getApplication());
@@ -215,4 +231,39 @@ class WriteMetadataJob extends AbstractJob
return false;
}
private function removeNulChar($value)
{
return str_replace("\0", "", $value);
}
/**
* re-format a phraseanet date for iptc writing
* return NULL if the date is not valid
*
* @param string $value
* @return string|null
*/
private static function fixDate($value)
{
$date = null;
try {
$a = explode(';', preg_replace('/\D+/', ';', trim($value)));
switch (count($a)) {
case 3: // yyyy;mm;dd
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2]);
$date = $date->format('Y-m-d H:i:s');
break;
case 6: // yyyy;mm;dd;hh;mm;ss
$date = new \DateTime($a[0] . '-' . $a[1] . '-' . $a[2] . ' ' . $a[3] . ':' . $a[4] . ':' . $a[5]);
$date = $date->format('Y-m-d H:i:s');
break;
}
}
catch (\Exception $e) {
$date = null;
}
return $date;
}
}

View File

@@ -1115,7 +1115,6 @@ class ACL implements cache_cacheableInterface
/**
* @param array $base_ids
* @return $this
* @throws DBALException
* @throws Exception
*/
public function revoke_access_from_bases(Array $base_ids)
@@ -1125,24 +1124,30 @@ class ACL implements cache_cacheableInterface
$usr_id = $this->user->getId();
$errors = 0;
foreach ($base_ids as $base_id) {
if (!$stmt_del->execute([':base_id' => $base_id, ':usr_id' => $usr_id])) {
throw new Exception('Error while deleteing some rights');
}
$this->app['dispatcher']->dispatch(
AclEvents::ACCESS_TO_BASE_REVOKED,
new AccessToBaseRevokedEvent(
$this,
array(
'base_id'=>$base_id
if ($stmt_del->execute([':base_id' => $base_id, ':usr_id' => $usr_id])) {
$this->app['dispatcher']->dispatch(
AclEvents::ACCESS_TO_BASE_REVOKED,
new AccessToBaseRevokedEvent(
$this,
[
'base_id' => $base_id
]
)
)
);
);
}
else {
$errors++;
}
}
$stmt_del->closeCursor();
$this->delete_data_from_cache(self::CACHE_RIGHTS_BAS);
if($errors > 0) {
throw new Exception('Error while deleting some rights');
}
return $this;
}

View File

@@ -121,9 +121,9 @@ class cache_databox
$conn = $app->getApplicationBox()->get_connection();
$sql = 'UPDATE sitepreff SET memcached_update = :date';
$sql = 'UPDATE sitepreff SET memcached_update = current_timestamp()';
$stmt = $conn->prepare($sql);
$stmt->execute([':date' => $now]);
$stmt->execute();
$stmt->closeCursor();
self::$refreshing = false;

View File

@@ -11,6 +11,9 @@
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\UserRepository;
class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
{
@@ -31,9 +34,9 @@ class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
/**
*
* @param Array $datas
* @param string[] $data
* @param boolean $unread
* @return string
* @return array
*/
public function datas(array $data, $unread)
{
@@ -41,24 +44,29 @@ class eventsmanager_notify_orderdeliver extends eventsmanager_notifyAbstract
$ssel_id = $data['ssel_id'];
$n = $data['n'];
if (null === $user= $this->app['repo.users']->find(($from))) {
/** @var UserRepository $userRepo */
$userRepo = $this->app['repo.users'];
if( ($user= $userRepo->find(($from))) === null ) {
return [];
}
$sender = $user->getDisplayName();
try {
/** @var BasketRepository $repository */
$repository = $this->app['repo.baskets'];
$basket = $repository->findUserBasket($ssel_id, $this->app->getAuthenticatedUser(), false);
} catch (\Exception $e) {
}
catch (\Exception $e) {
return [];
}
$ret = [
'text' => $this->app->trans('%user% vous a delivre %quantity% document(s) pour votre commande %title%', ['%user%' => $sender, '%quantity%' => $n, '%title%' => '<a href="/lightbox/compare/'
. $ssel_id . '/" target="_blank">'
. $basket->getName() . '</a>'])
, 'class' => ''
. $basket->getName() . '</a>']),
'class' => ''
];
return $ret;

View File

@@ -11,6 +11,9 @@
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\UserRepository;
class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
{
@@ -31,35 +34,38 @@ class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
/**
*
* @param string $datas
* @param string[] $data
* @param boolean $unread
* @return Array
* @return array
*/
public function datas(array $data, $unread)
{
$from = $data['from'];
$ssel_id = $data['ssel_id'];
if (null === $registered_user = $this->app['repo.users']->find($from)) {
/** @var UserRepository $userRepo */
$userRepo = $this->app['repo.users'];
if ( ($registered_user = $userRepo->find($from)) === null ) {
return [];
}
$sender = $registered_user->getDisplayName();
try {
/** @var BasketRepository $repository */
$repository = $this->app['repo.baskets'];
$basket = $repository->findUserBasket($ssel_id, $this->app->getAuthenticatedUser(), false);
} catch (\Exception $e) {
}
catch (\Exception $e) {
return [];
}
$ret = [
'text' => $this->app->trans('%user% a envoye son rapport de validation de %title%', ['%user%' => $sender, '%title%' => '<a href="/lightbox/validate/'
. $ssel_id . '/" target="_blank">'
. $basket->getName() . '</a>'
])
, 'class' => ''
. $basket->getName() . '</a>']),
'class' => ''
];
return $ret;
@@ -84,12 +90,18 @@ class eventsmanager_notify_validationdone extends eventsmanager_notifyAbstract
}
/**
* @param integer $usr_id The id of the user to check
* @param User $user The id of the user to check
*
* @return boolean
*/
public function is_available(User $user)
{
return $this->app->getAclForUser($user)->has_right(\ACL::CANPUSH);
try {
return $this->app->getAclForUser($user)->has_right(\ACL::CANPUSH);
}
catch (\Exception $e) {
// has_right(unknow_right) ? will not happen !
return false;
}
}
}

View File

@@ -9,13 +9,16 @@
*/
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
use Alchemy\Phrasea\Model\Entities\Token;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
use Assert\Assertion;
use Doctrine\DBAL\Connection;
use Symfony\Component\Filesystem\Filesystem;
class set_export extends set_abstract
{
private static $maxFilenameLength = 256;
@@ -60,6 +63,7 @@ class set_export extends set_abstract
$remain_hd = [];
if ($storyWZid) {
/** @var StoryWZRepository $repository */
$repository = $app['repo.story-wz'];
$storyWZ = $repository->findByUserAndId($this->app, $app->getAuthenticatedUser(), $storyWZid);
@@ -68,6 +72,7 @@ class set_export extends set_abstract
}
if ($sstid != "") {
/** @var BasketRepository $repository */
$repository = $app['repo.baskets'];
$Basket = $repository->findUserBasket($sstid, $app->getAuthenticatedUser(), false);

View File

@@ -1278,6 +1278,27 @@
<field>id</field>
</fields>
</index>
<index>
<name>usr_id</name>
<type>INDEX</type>
<fields>
<field>usr_id</field>
</fields>
</index>
<index>
<name>unread</name>
<type>INDEX</type>
<fields>
<field>unread</field>
</fields>
</index>
<index>
<name>created_on</name>
<type>INDEX</type>
<fields>
<field>created_on</field>
</fields>
</index>
</indexes>
<engine>InnoDB</engine>
</table>

View File

@@ -234,6 +234,7 @@ embed_bundle:
audio:
player: videojs
autoplay: false
cover_subdef: thumbnail
document:
#player: flexpaper
enable_pdfjs: true

View File

@@ -65,7 +65,7 @@
"normalize-css": "^2.1.0",
"npm": "^6.0.0",
"npm-modernizr": "^2.8.3",
"phraseanet-production-client": "^0.34.16-d",
"phraseanet-production-client": "0.34.72-d",
"requirejs": "^2.3.5",
"tinymce": "^4.0.28",
"underscore": "^1.8.3",

View File

@@ -0,0 +1,21 @@
---
- hosts: all
sudo: true
vars_files:
- vars/all.yml
roles:
# - server
# - repositories
# - vagrant_local
- nginx
# - mariadb
# - elasticsearch
# - rabbitmq
# - php
- xdebug
# - composer
- mailcatcher
# - node
# - yarn
# - ffmpeg
- app

View File

@@ -4,18 +4,18 @@
vars_files:
- vars/all.yml
roles:
# - server
# - repositories
# - vagrant_local
- server
- repositories
- vagrant_local
- nginx
# - mariadb
# - elasticsearch
# - rabbitmq
# - php
- mariadb
- elasticsearch
- rabbitmq
- php
- xdebug
# - composer
# - mailcatcher
# - node
# - yarn
# - ffmpeg
- composer
- mailcatcher
- node
- yarn
- ffmpeg
- app

View File

@@ -13,7 +13,7 @@
changed_when: false
- name: Install Dependencies
apt: pkg=openjdk-7-jre state=latest
apt: pkg=openjdk-8-jre state=latest
- name: Remove temporary debian package
shell: rm -f /tmp/elasticsearch-{{ elasticsearch.version }}.deb

View File

@@ -13,7 +13,7 @@
- name: Install mailcatcher gem
# gem module is flaky, this is consistent
command: gem install mailcatcher --conservative
command: gem install mailcatcher -v 0.6.4 --conservative
ignore_errors: yes
- name: Install mailcatcher supervisord conf

View File

@@ -16,7 +16,7 @@
- name: Add Key for MariaDB Repository
sudo: yes
apt_key: url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xcbcb082a1bb943db
apt_key: url=http://keyserver.ubuntu.com/pks/lookup?op=get&search=0xF1656F24C74CD1D8
# RabbitMQ
- name: Add rabbitmq package repository

View File

@@ -34,12 +34,14 @@ server:
- fr_FR.UTF-8
- de_DE.UTF-8
- nl_NL.UTF-8
repositories:
php: 'ppa:ondrej/php'
mariadb: 'deb http://mirror6.layerjet.com/mariadb/repo/10.1/ubuntu'
mariadb: 'deb [arch=amd64,arm64,i386,ppc64el] http://mirror.nodesdirect.com/mariadb/repo/10.3/ubuntu'
elasticsearch: 'ppa:webupd8team/java'
rabbitmq: 'deb http://www.rabbitmq.com/debian/ testing main'
yarn: 'https://dl.yarnpkg.com/debian/'
vagrant_local:
install: '1'
vm:

View File

@@ -148,6 +148,9 @@ $mainMenuLinkBackgroundHoverColor: transparent;
}
#FNDR a {
text-decoration: none;
img {
width: 16px;
}
}
#FNDR a:hover {
text-decoration: none;

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 997 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

View File

@@ -31,17 +31,13 @@ var commonModule = (function ($, p4) {
$(this).removeClass('context-menu-item-hover');
});
// $('#help-trigger').contextMenu('#mainMenu .helpcontextmenu', {openEvt: 'click', dropDown: true, theme: 'vista', dropDown: true,
// showTransition: 'slideDown',
// hideTransition: 'hide',
// shadow: false
// });
$('body').on('click', '.infoDialog', function (event) {
infoDialog($(this));
});
});
function showOverlay(n, appendto, callback, zIndex) {
var div = "OVERLAY";

View File

@@ -0,0 +1,167 @@
$colorPickerImagesPath: '/assets/common/images/' !default;
.colorpicker {
width: 356px;
height: 176px;
overflow: hidden;
position: absolute;
background: url('#{$colorPickerImagesPath}colorpicker_background.png');
font-family: Arial, Helvetica, sans-serif;
display: none;
}
.colorpicker_color {
width: 150px;
height: 150px;
left: 14px;
top: 13px;
position: absolute;
background: #f00;
overflow: hidden;
cursor: crosshair;
}
.colorpicker_color div {
position: absolute;
top: 0;
left: 0;
width: 150px;
height: 150px;
background: url('#{$colorPickerImagesPath}colorpicker_overlay.png');
}
.colorpicker_color div div {
position: absolute;
top: 0;
left: 0;
width: 11px;
height: 11px;
overflow: hidden;
background: url('#{$colorPickerImagesPath}colorpicker_select.gif');
margin: -5px 0 0 -5px;
}
.colorpicker_hue {
position: absolute;
top: 13px;
left: 171px;
width: 35px;
height: 150px;
cursor: n-resize;
}
.colorpicker_hue div {
position: absolute;
width: 35px;
height: 9px;
overflow: hidden;
background: url('#{$colorPickerImagesPath}colorpicker_indic.gif') left top;
margin: -4px 0 0 0;
left: 0px;
}
.colorpicker_new_color {
position: absolute;
width: 60px;
height: 30px;
left: 213px;
top: 13px;
background: #f00;
}
.colorpicker_current_color {
position: absolute;
width: 60px;
height: 30px;
left: 283px;
top: 13px;
background: #f00;
}
.colorpicker input {
background-color: transparent;
border: 1px solid transparent;
position: absolute;
font-size: 10px;
font-family: Arial, Helvetica, sans-serif;
color: #898989;
top: 4px;
right: 11px;
text-align: right;
margin: 0;
padding: 0;
height: 11px;
}
.colorpicker_hex {
position: absolute;
width: 72px;
height: 22px;
background: url('#{$colorPickerImagesPath}colorpicker_hex.png') top;
left: 212px;
top: 142px;
}
.colorpicker_hex input {
right: 6px;
}
.colorpicker_field {
height: 22px;
width: 62px;
background-position: top;
position: absolute;
}
.colorpicker_field span {
position: absolute;
width: 12px;
height: 22px;
overflow: hidden;
top: 0;
right: 0;
cursor: n-resize;
}
.colorpicker_rgb_r {
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_r.png');
top: 52px;
left: 212px;
}
.colorpicker_rgb_g {
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_g.png');
top: 82px;
left: 212px;
}
.colorpicker_rgb_b {
background-image: url('#{$colorPickerImagesPath}colorpicker_rgb_b.png');
top: 112px;
left: 212px;
}
.colorpicker_hsb_h {
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_h.png');
top: 52px;
left: 282px;
}
.colorpicker_hsb_s {
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_s.png');
top: 82px;
left: 282px;
}
.colorpicker_hsb_b {
background-image: url('#{$colorPickerImagesPath}colorpicker_hsb_b.png');
top: 112px;
left: 282px;
}
.colorpicker_submit {
position: absolute;
width: 22px;
height: 22px;
background: url('#{$colorPickerImagesPath}colorpicker_submit.png') top;
left: 322px;
top: 142px;
overflow: hidden;
.submiter {
color: #ffffff;
}
}
.colorpicker_focus {
background-position: center;
}
.colorpicker_hex.colorpicker_focus {
background-position: bottom;
}
.colorpicker_submit.colorpicker_focus {
background-position: bottom;
}
.colorpicker_slider {
background-position: bottom;
}

View File

View File

File diff suppressed because it is too large Load Diff

Binary file not shown.

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