1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-05 12:50:29 +02:00

First commit

This commit is contained in:
Laurent Cozic 2016-10-19 16:54:41 +01:00
commit 581f188927
79 changed files with 6723 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
/app/config/parameters.yml
/build/
/phpunit.xml
/var/*
!/var/cache
/var/cache/*
!var/cache/.gitkeep
!/var/logs
/var/logs/*
!var/logs/.gitkeep
!/var/sessions
/var/sessions/*
!var/sessions/.gitkeep
!var/SymfonyRequirements.php
/vendor/
/web/bundles/
*.sublime-workspace

4
README.md Normal file
View File

@ -0,0 +1,4 @@
notes
=====
A Symfony project created on October 5, 2016, 4:15 pm.

7
app/.htaccess Normal file
View File

@ -0,0 +1,7 @@
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>

7
app/AppCache.php Normal file
View File

@ -0,0 +1,7 @@
<?php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
class AppCache extends HttpCache
{
}

50
app/AppKernel.php Normal file
View File

@ -0,0 +1,50 @@
<?php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
new Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
new Symfony\Bundle\SecurityBundle\SecurityBundle(),
new Symfony\Bundle\TwigBundle\TwigBundle(),
new Symfony\Bundle\MonologBundle\MonologBundle(),
new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
new AppBundle\AppBundle(),
];
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
$bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle();
$bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
$bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle();
$bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
}
return $bundles;
}
public function getRootDir()
{
return __DIR__;
}
public function getCacheDir()
{
return dirname(__DIR__).'/var/cache/'.$this->getEnvironment();
}
public function getLogDir()
{
return dirname(__DIR__).'/var/logs';
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}{% endblock %}
<link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,76 @@
{% extends 'base.html.twig' %}
{% block body %}
<div id="wrapper">
<div id="container">
<div id="welcome">
<h1><span>Welcome to</span> Symfony {{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION') }}</h1>
</div>
<div id="status">
<p>
<svg id="icon-status" width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1671 566q0 40-28 68l-724 724-136 136q-28 28-68 28t-68-28l-136-136-362-362q-28-28-28-68t28-68l136-136q28-28 68-28t68 28l294 295 656-657q28-28 68-28t68 28l136 136q28 28 28 68z" fill="#759E1A"/></svg>
Your application is now ready. You can start working on it at:
<code>{{ base_dir }}</code>
</p>
</div>
<div id="next">
<h2>What's next?</h2>
<p>
<svg id="icon-book" version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="-12.5 9 64 64" enable-background="new -12.5 9 64 64" xml:space="preserve">
<path fill="#AAA" d="M6.8,40.8c2.4,0.8,4.5-0.7,4.9-2.5c0.2-1.2-0.3-2.1-1.3-3.2l-0.8-0.8c-0.4-0.5-0.6-1.3-0.2-1.9
c0.4-0.5,0.9-0.8,1.8-0.5c1.3,0.4,1.9,1.3,2.9,2.2c-0.4,1.4-0.7,2.9-0.9,4.2l-0.2,1c-0.7,4-1.3,6.2-2.7,7.5
c-0.3,0.3-0.7,0.5-1.3,0.6c-0.3,0-0.4-0.3-0.4-0.3c0-0.3,0.2-0.3,0.3-0.4c0.2-0.1,0.5-0.3,0.4-0.8c0-0.7-0.6-1.3-1.3-1.3
c-0.6,0-1.4,0.6-1.4,1.7s1,1.9,2.4,1.8c0.8,0,2.5-0.3,4.2-2.5c2-2.5,2.5-5.4,2.9-7.4l0.5-2.8c0.3,0,0.5,0.1,0.8,0.1
c2.4,0.1,3.7-1.3,3.7-2.3c0-0.6-0.3-1.2-0.9-1.2c-0.4,0-0.8,0.3-1,0.8c-0.1,0.6,0.8,1.1,0.1,1.5c-0.5,0.3-1.4,0.6-2.7,0.4l0.3-1.3
c0.5-2.6,1-5.7,3.2-5.8c0.2,0,0.8,0,0.8,0.4c0,0.2,0,0.2-0.2,0.5c-0.2,0.3-0.3,0.4-0.2,0.7c0,0.7,0.5,1.1,1.2,1.1
c0.9,0,1.2-1,1.2-1.4c0-1.2-1.2-1.8-2.6-1.8c-1.5,0.1-2.8,0.9-3.7,2.1c-1.1,1.3-1.8,2.9-2.3,4.5c-0.9-0.8-1.6-1.8-3.1-2.3
c-1.1-0.7-2.3-0.5-3.4,0.3c-0.5,0.4-0.8,1-1,1.6c-0.4,1.5,0.4,2.9,0.8,3.4l0.9,1c0.2,0.2,0.6,0.8,0.4,1.5c-0.3,0.8-1.2,1.3-2.1,1
c-0.4-0.2-1-0.5-0.9-0.9c0.1-0.2,0.2-0.3,0.3-0.5s0.1-0.3,0.1-0.3c0.2-0.6-0.1-1.4-0.7-1.6c-0.6-0.2-1.2,0-1.3,0.8
C4.3,38.4,4.7,40,6.8,40.8z M46.1,20.9c0-4.2-3.2-7.5-7.1-7.5h-3.8C34.8,10.8,32.7,9,30.2,9L-2.3,9.1c-2.8,0.1-4.9,2.4-4.9,5.4
L-7,58.6c0,4.8,8.1,13.9,11.6,14.1l34.7-0.1c3.9,0,7-3.4,7-7.6L46.1,20.9z M-0.3,36.4c0-8.6,6.5-15.6,14.5-15.6
c8,0,14.5,7,14.5,15.6S22.1,52,14.2,52C6.1,52-0.3,45-0.3,36.4z M42.1,65.1c0,1.8-1.5,3.1-3.1,3.1H4.6c-0.7,0-3-1.8-4.5-4.4h30.4
c2.8,0,5-2.4,5-5.4V17.9h3.7c1.6,0,2.9,1.4,2.9,3.1V65.1L42.1,65.1z"/>
</svg>
Read the documentation to learn
<a href="http://symfony.com/doc/{{ constant('Symfony\\Component\\HttpKernel\\Kernel::VERSION')[:3] }}/book/page_creation.html">
How to create your first page in Symfony
</a>
</p>
</div>
</div>
</div>
{% endblock %}
{% block stylesheets %}
<style>
body { background: #F5F5F5; font: 18px/1.5 sans-serif; }
h1, h2 { line-height: 1.2; margin: 0 0 .5em; }
h1 { font-size: 36px; }
h2 { font-size: 21px; margin-bottom: 1em; }
p { margin: 0 0 1em 0; }
a { color: #0000F0; }
a:hover { text-decoration: none; }
code { background: #F5F5F5; max-width: 100px; padding: 2px 6px; word-wrap: break-word; }
#wrapper { background: #FFF; margin: 1em auto; max-width: 800px; width: 95%; }
#container { padding: 2em; }
#welcome, #status { margin-bottom: 2em; }
#welcome h1 span { display: block; font-size: 75%; }
#icon-status, #icon-book { float: left; height: 64px; margin-right: 1em; margin-top: -4px; width: 64px; }
#icon-book { display: none; }
@media (min-width: 768px) {
#wrapper { width: 80%; margin: 2em auto; }
#icon-book { display: inline-block; }
#status a, #next a { display: block; }
@-webkit-keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
@keyframes fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
.sf-toolbar { opacity: 0; -webkit-animation: fade-in 1s .2s forwards; animation: fade-in 1s .2s forwards;}
}
</style>
{% endblock %}

11
app/autoload.php Normal file
View File

@ -0,0 +1,11 @@
<?php
use Doctrine\Common\Annotations\AnnotationRegistry;
use Composer\Autoload\ClassLoader;
/** @var ClassLoader $loader */
$loader = require __DIR__.'/../vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
return $loader;

68
app/config/config.yml Normal file
View File

@ -0,0 +1,68 @@
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
# Put parameters here that don't need to change on each machine where the app is deployed
# http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: en
framework:
#esi: ~
#translator: { fallbacks: ["%locale%"] }
secret: "%secret%"
router:
resource: "%kernel.root_dir%/config/routing.yml"
strict_requirements: ~
form: ~
csrf_protection: ~
validation: { enable_annotations: true }
#serializer: { enable_annotations: true }
templating:
engines: ['twig']
default_locale: "%locale%"
trusted_hosts: ~
trusted_proxies: ~
session:
# http://symfony.com/doc/current/reference/configuration/framework.html#handler-id
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
fragments: ~
http_method_override: true
assets: ~
# Twig Configuration
twig:
debug: "%kernel.debug%"
strict_variables: "%kernel.debug%"
# Doctrine Configuration
doctrine:
dbal:
driver: pdo_mysql
host: "%database_host%"
port: "%database_port%"
dbname: "%database_name%"
user: "%database_user%"
password: "%database_password%"
charset: UTF8
# if using pdo_sqlite as your database driver:
# 1. add the path in parameters.yml
# e.g. database_path: "%kernel.root_dir%/data/data.db3"
# 2. Uncomment database_path in parameters.yml.dist
# 3. Uncomment next line:
# path: "%database_path%"
orm:
auto_generate_proxy_classes: "%kernel.debug%"
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool: { type: memory }

34
app/config/config_dev.yml Normal file
View File

@ -0,0 +1,34 @@
imports:
- { resource: config.yml }
framework:
router:
resource: "%kernel.root_dir%/config/routing_dev.yml"
strict_requirements: true
profiler: { only_exceptions: false }
web_profiler:
toolbar: true
intercept_redirects: false
monolog:
handlers:
main:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: [!event]
console:
type: console
channels: [!event, !doctrine]
# uncomment to get logging in your browser
# you may have to allow bigger header sizes in your Web server configuration
#firephp:
# type: firephp
# level: info
#chromephp:
# type: chromephp
# level: info
#swiftmailer:
# delivery_address: me@example.com

View File

@ -0,0 +1,21 @@
imports:
- { resource: config.yml }
#doctrine:
# orm:
# metadata_cache_driver: apc
# result_cache_driver: apc
# query_cache_driver: apc
monolog:
handlers:
main:
type: fingers_crossed
action_level: error
handler: nested
nested:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
console:
type: console

View File

@ -0,0 +1,16 @@
imports:
- { resource: config_dev.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
swiftmailer:
disable_delivery: true

View File

@ -0,0 +1,19 @@
# This file is a "template" of what your parameters.yml file should look like
# Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production.
# http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration
parameters:
database_host: 127.0.0.1
database_port: ~
database_name: symfony
database_user: root
database_password: ~
# You should uncomment this if you want use pdo_sqlite
# database_path: "%kernel.root_dir%/data.db3"
mailer_transport: smtp
mailer_host: 127.0.0.1
mailer_user: ~
mailer_password: ~
# A secret key that's used to generate certain security-related tokens
secret: ThisTokenIsNotSoSecretChangeIt

3
app/config/routing.yml Normal file
View File

@ -0,0 +1,3 @@
app:
resource: "@AppBundle/Controller/"
type: annotation

View File

@ -0,0 +1,14 @@
_wdt:
resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml"
prefix: /_wdt
_profiler:
resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml"
prefix: /_profiler
_errors:
resource: "@TwigBundle/Resources/config/routing/errors.xml"
prefix: /_error
_main:
resource: routing.yml

24
app/config/security.yml Normal file
View File

@ -0,0 +1,24 @@
# To get started with security, check out the documentation:
# http://symfony.com/doc/current/book/security.html
security:
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
in_memory:
memory: ~
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
# activate different ways to authenticate
# http_basic: ~
# http://symfony.com/doc/current/book/security.html#a-configuring-how-your-users-will-authenticate
# form_login: ~
# http://symfony.com/doc/current/cookbook/security/form_login_setup.html

17
app/config/services.yml Normal file
View File

@ -0,0 +1,17 @@
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters:
# parameter_name: value
services:
app.eloquent:
class: AppBundle\Eloquent
arguments: []
# app.fine_diff:
# class: AppBundle\FineDiff
# arguments: []
twig.exception_listener:
class: stdObject

27
bin/console Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env php
<?php
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Debug\Debug;
// if you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#configuration-and-setup for more information
//umask(0000);
set_time_limit(0);
/** @var Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__.'/../app/autoload.php';
$input = new ArgvInput();
$env = $input->getParameterOption(['--env', '-e'], getenv('SYMFONY_ENV') ?: 'dev');
$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(['--no-debug', '']) && $env !== 'prod';
if ($debug) {
Debug::enable();
}
$kernel = new AppKernel($env, $debug);
$application = new Application($kernel);
$application->run($input);

146
bin/symfony_requirements Executable file
View File

@ -0,0 +1,146 @@
#!/usr/bin/env php
<?php
require_once dirname(__FILE__).'/../var/SymfonyRequirements.php';
$lineSize = 70;
$symfonyRequirements = new SymfonyRequirements();
$iniPath = $symfonyRequirements->getPhpIniConfigPath();
echo_title('Symfony Requirements Checker');
echo '> PHP is using the following php.ini file:'.PHP_EOL;
if ($iniPath) {
echo_style('green', ' '.$iniPath);
} else {
echo_style('yellow', ' WARNING: No configuration file (php.ini) used by PHP!');
}
echo PHP_EOL.PHP_EOL;
echo '> Checking Symfony requirements:'.PHP_EOL.' ';
$messages = array();
foreach ($symfonyRequirements->getRequirements() as $req) {
if ($helpText = get_error_message($req, $lineSize)) {
echo_style('red', 'E');
$messages['error'][] = $helpText;
} else {
echo_style('green', '.');
}
}
$checkPassed = empty($messages['error']);
foreach ($symfonyRequirements->getRecommendations() as $req) {
if ($helpText = get_error_message($req, $lineSize)) {
echo_style('yellow', 'W');
$messages['warning'][] = $helpText;
} else {
echo_style('green', '.');
}
}
if ($checkPassed) {
echo_block('success', 'OK', 'Your system is ready to run Symfony projects');
} else {
echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects');
echo_title('Fix the following mandatory requirements', 'red');
foreach ($messages['error'] as $helpText) {
echo ' * '.$helpText.PHP_EOL;
}
}
if (!empty($messages['warning'])) {
echo_title('Optional recommendations to improve your setup', 'yellow');
foreach ($messages['warning'] as $helpText) {
echo ' * '.$helpText.PHP_EOL;
}
}
echo PHP_EOL;
echo_style('title', 'Note');
echo ' The command console could use a different php.ini file'.PHP_EOL;
echo_style('title', '~~~~');
echo ' than the one used with your web server. To be on the'.PHP_EOL;
echo ' safe side, please check the requirements from your web'.PHP_EOL;
echo ' server using the ';
echo_style('yellow', 'web/config.php');
echo ' script.'.PHP_EOL;
echo PHP_EOL;
exit($checkPassed ? 0 : 1);
function get_error_message(Requirement $requirement, $lineSize)
{
if ($requirement->isFulfilled()) {
return;
}
$errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL;
$errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL;
return $errorMessage;
}
function echo_title($title, $style = null)
{
$style = $style ?: 'title';
echo PHP_EOL;
echo_style($style, $title.PHP_EOL);
echo_style($style, str_repeat('~', strlen($title)).PHP_EOL);
echo PHP_EOL;
}
function echo_style($style, $message)
{
// ANSI color codes
$styles = array(
'reset' => "\033[0m",
'red' => "\033[31m",
'green' => "\033[32m",
'yellow' => "\033[33m",
'error' => "\033[37;41m",
'success' => "\033[37;42m",
'title' => "\033[34m",
);
$supports = has_color_support();
echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : '');
}
function echo_block($style, $title, $message)
{
$message = ' '.trim($message).' ';
$width = strlen($message);
echo PHP_EOL.PHP_EOL;
echo_style($style, str_repeat(' ', $width));
echo PHP_EOL;
echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT));
echo PHP_EOL;
echo_style($style, $message);
echo PHP_EOL;
echo_style($style, str_repeat(' ', $width));
echo PHP_EOL;
}
function has_color_support()
{
static $support;
if (null === $support) {
if (DIRECTORY_SEPARATOR == '\\') {
$support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
} else {
$support = function_exists('posix_isatty') && @posix_isatty(STDOUT);
}
}
return $support;
}

View File

@ -0,0 +1 @@
{"last_sync_id":81,"file_map":[],"test":"abcd","client_id":"11111111111111111111111111111111","last_sync_time":1476289357,"session_id":null,"folder_items":[]}

View File

@ -0,0 +1 @@
{"last_sync_id":80,"file_map":[],"client_id":"22222222222222222222222222222222","last_sync_time":1476289618,"folder_items":[],"session_id":null}

2
cli-client/client1.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
php main.php --config ~/src/notes/cli-client/.config1 "$@"

2
cli-client/client2.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
php main.php --config ~/src/notes/cli-client/.config2 "$@"

174
cli-client/main.go Normal file
View File

@ -0,0 +1,174 @@
package main
import (
//"bufio"
//"errors"
"fmt"
"os"
"io"
"io/ioutil"
// "os/exec"
// "os/user"
"path/filepath"
"path"
"crypto/md5"
// "runtime"
// "strconv"
"strings"
// "time"
"net/url"
"net/http"
"github.com/jessevdk/go-flags"
)
const VERSION = "1.0.0"
type SyncCommandOptions struct {
// FfmpegPath string `long:"ffmpeg" description:"Path to ffmpeg." default:"ffmpeg"`
// FrameDirPath string `short:"d" long:"frame-dir" description:"Path to directory that will contain the captured frames. (default: <PictureDirectory>/pmcctv)"`
// RemoteDir string `short:"r" long:"remote-dir" description:"Remote location where frames will be saved to. Must contain a path compatible with scp (eg. user@someip:~/pmcctv)."`
// RemotePort string `short:"p" long:"remote-port" description:"Port of remote location where frames will be saved to. If not set, whatever is the default scp port will be used (should be 22)."`
// BurstModeDuration int `short:"b" long:"burst-mode-duration" description:"Duration of burst mode, in seconds. Set to 0 to disable burst mode altogether." default:"10"`
// BurstModeFormat string `short:"f" long:"burst-mode-format" description:"Format of burst mode captured files, either \"image\" or \"video\"." default:"video"`
// FramesTtl int `short:"t" long:"time-to-live" description:"For how long captured frames should be kept, in days." default:"7"`
// InputDevice string `short:"i" long:"input-device" description:"Name of capture input device. (default: auto-detect)"`
}
type AppCommandOptions struct {
Version bool `long:"version" description:"Display version information"`
}
type CommandOptions struct {
App AppCommandOptions
Sync SyncCommandOptions
}
func printHelp(flagParser *flags.Parser) {
flagParser.WriteHelp(os.Stdout)
fmt.Printf("\n")
fmt.Printf("For help with a particular command, type \"%s <command> --help\"\n", path.Base(os.Args[0]))
}
func createFlagParser() (CommandOptions, *flags.Parser) {
var opts CommandOptions
flagParser := flags.NewParser(&opts.App, flags.HelpFlag|flags.PassDoubleDash)
flagParser.AddCommand(
"sync",
"Synchronize notes",
"Synchronize local notes with the server.",
&opts.Sync,
)
return opts, flagParser
}
func createId(path string) string {
h := md5.New()
io.WriteString(h, "31208854954776365651")
io.WriteString(h, path)
return fmt.Sprintf("%x", h.Sum(nil))
}
func readFile(path string) (string, error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return string(content), nil
}
func makeApiCall(method string, path string, data url.Values) (error, []byte) {
baseUrl := "http://127.0.0.1:8000"
fullUrl := baseUrl + "/" + path
client := http.Client{}
request, err := http.NewRequest(method, fullUrl, strings.NewReader(data.Encode()))
if err != nil {
return err, []byte{}
}
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
response, err := client.Do(request)
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return err, []byte{}
}
return nil, body
}
// type Note struct {
// Id string
// Title string
// Body string
// }
func main() {
var err error
//err, body := makeApiCall("GET", "users/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", url.Values{})
note := url.Values{}
note.Add("title", "from go")
note.Add("body", "body from go")
err, body := makeApiCall("POST", "notes", note)
fmt.Println(err)
fmt.Println(string(body))
os.Exit(0)
opts, flagParser := createFlagParser()
args, err := flagParser.Parse()
if err != nil {
t := err.(*flags.Error).Type
if t == flags.ErrHelp {
printHelp(flagParser)
os.Exit(0)
} else if t == flags.ErrCommandRequired {
// Here handle default flags (which are not associated with any command)
if opts.App.Version {
fmt.Println(VERSION)
os.Exit(0)
}
printHelp(flagParser)
os.Exit(0)
} else {
fmt.Printf("Error: %s\n", err)
fmt.Printf("Type '%s --help' for more information.\n", path.Base(os.Args[0]))
os.Exit(1)
}
}
_ = args
fullPath := "/home/laurent/src/notes/cli-client/test"
walkPath := func (path string, info os.FileInfo, err error) error {
if len(path) - len(fullPath) <= 0 {
return nil
}
p := path[len(fullPath)+1:];
fmt.Println(p)
fmt.Println(createId(p))
if !info.IsDir() {
content, err := readFile(path)
if err != nil {
fmt.Println(err)
return err
}
fmt.Println(content)
}
return nil
}
filepath.Walk(fullPath, walkPath)
}

349
cli-client/main.php Normal file
View File

@ -0,0 +1,349 @@
<?php
function escapePathElement($element) {
$valid = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ÀàÁáÂâÃãÄäÇçÈèÉéÊêËëÌìÍíÎîÏïÑnÒòÓóÔôÕõÖöŠšÚùÛúÜûÙüÝyŸÿŽz_- ().,';
$output = '';
$chars = preg_split('//u', $element, -1, PREG_SPLIT_NO_EMPTY); // Split a UTF-8 string into characters
foreach ($chars as $c) {
if (strpos($valid, $c) !== false) {
$output .= $c;
} else {
$output .= rawurlencode($c);
}
}
return $output;
}
function escapePath($path) {
$output = '';
$elements = preg_split('/[\\\\\/]/', $path);
for ($i = 0; $i < count($elements); $i++) {
$e = $elements[$i];
if ($i > 0) $output .= '/';
$output .= escapePathElement($e);
}
return $output;
}
class Api {
private $sessionId = null;
private $baseUrl = null;
public function __construct($baseUrl) {
$this->baseUrl = $baseUrl;
}
static public function createId($string) {
// TODO: This needs to be unique per user
return md5('gKcr0 ^L3UL^fJV%1IW~~/Q`.,WRAr</8@$.k|uyK-w^d:k|{h!%(};|)OY9^lu=' . $string);
}
public function setSessionId($sessionId) {
$this->sessionId = $sessionId;
}
public function toCurlCmd($method, $url, $data = null) {
$cmd = 'curl';
$addMethod = true;
if ($method == 'GET') $addMethod = false;
if ($method == 'POST' && count($data)) $addMethod = false;
if ($addMethod) $cmd .= ' -X ' . $method;
if ($data) {
foreach ($data as $k => $v) {
$cmd .= ' -F "' . $k . '=' . rawurlencode($v) . '"';
}
}
$cmd .= ' ' . "'" . $url . "'";
return $cmd;
}
public function exec($method, $path, $query = null, $data = null) {
$url = $this->baseUrl . '/' . $path;
if (!$query) $query = array();
if ($this->sessionId) $query['session'] = $this->sessionId;
if (count($query)) $url .= '?' . http_build_query($query);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if ($data) curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
if ($method == 'PATCH' || $method == 'PUT' || $method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
}
$cmd = $this->toCurlCmd($method, $url, $data);
echo $cmd . "\n";
$content = curl_exec($ch);
curl_close($ch);
$output = json_decode($content, true);
if ($output === null) throw new Exception('Invalid response: ' . $content . "\n\nCommand: " . $cmd . "\n");
if (isset($output['error'])) throw new Exception('API error: ' . $content . "\n\nCommand: " . $cmd . "\n");
return $output;
}
public function login($email, $password, $clientId) {
$method = 'POST';
$path = 'sessions';
$data = array(
'email' => $email,
'password' => $password,
'client_id' => $clientId,
);
return $this->exec($method, $path, null, $data);
}
}
class Config {
protected $dirPath = null;
public function __construct($dirPath) {
$this->dirPath = $dirPath;
}
protected function load() {
$c = @file_get_contents($this->dirPath . '/config.json');
$c = json_decode($c, true);
if ($c === null) $c = array();
if (!isset($c['last_sync_id'])) $c['last_sync_id'] = 0;
if (!isset($c['last_sync_time'])) $c['last_sync_time'] = 0;
if (!isset($c['folder_items'])) $c['folder_items'] = array();
if (!isset($c['client_id'])) $c['client_id'] = null;
if (!isset($c['session_id'])) $c['session_id'] = null;
return $c;
}
protected function save($c) {
file_put_contents($this->dirPath . '/config.json', json_encode($c));
}
public function get($name) {
$c = $this->load();
if (!isset($c[$name])) throw new Exception('Invalid key name: ' . $name);
return $c[$name];
}
public function set($name, $value) {
$c = $this->load();
$c[$name] = $value;
$this->save($c);
}
}
class FolderItem {
private $title = '';
private $body = '';
private $id;
private $parentId;
private $isFolder;
private $modTime;
public function setTitle($v) { $this->title = $v; }
public function setBody($v) { $this->body = $v; }
public function setId($v) { $this->id = $v; }
public function setParentId($v) { $this->parentId = $v; }
public function setIsFolder($v) { $this->isFolder = $v; }
public function setModTime($v) { $this->modTime = $v; }
public function title() { return $this->title; }
public function body() { return $this->body; }
public function id() { return $this->id; }
public function parentId() { return $this->parentId; }
public function isFolder() { return $this->isFolder; }
public function isNote() { return !$this->isFolder(); }
public function modTime() { return $this->modTime; }
public function toApiArray() {
$output = array(
'title' => $this->title(),
'parent_id' => $this->parentId(),
);
if ($this->isNote()) $output['body'] = $this->body();
return $output;
}
public function fromApiArray($type, $array) {
$this->setTitle($array['title']);
if ($type == 'note') $this->setBody($array['body']);
$this->setId($array['id']);
$this->setParentId($array['parent_id']);
$this->setIsFolder($type == 'folder');
}
}
class FolderItems {
private $items = array();
private function getFolderItems($dir, $parentId, &$output) {
$paths = glob($dir . '/*');
foreach ($paths as $path) {
$isFolder = is_dir($path);
$modTime = filemtime($path);
$o = new FolderItem();
$o->setTitle(basename($path));
$o->setId(Api::createId($parentId . '_' . $o->title()));
$o->setParentId($parentId);
$o->setIsFolder($isFolder);
$o->setModTime($modTime);
if (!$isFolder) $o->setBody(file_get_contents($path));
$output[] = $o;
if ($isFolder) $this->getFolderItems($path, $o->id(), $output);
}
}
public function fromPath($path) {
$this->items = array();
$this->getFolderItems($path, null, $this->items);
}
public function all() {
return $this->items;
}
public function add($item) {
$this->items[] = $item;
}
public function setById($id, $item) {
$found = false;
for ($i = 0; $i < count($this->items); $i++) {
$it = $this->items[$i];
if ($it->id() == $id) {
$found = true;
$this->items[$i] = $item;
break;
}
}
if (!$found) {
$this->items[] = $item;
}
}
public function byId($id) {
foreach ($this->all() as $item) {
if ($item->id() == $id) return $item;
}
return null;
}
public function itemFullPath($item) {
if (!$item->parentId()) return $item->title();
$parent = $this->byId($item->parentId());
if (!$parent) throw new Exception('Cannot find parent with ID ' . $item->parentId());
return escapePath($this->itemFullPath($parent) . '/' . $item->title());
}
}
$shortopts = "";
$longopts = array(
"config:",
"sync",
);
$flags = getopt($shortopts, $longopts);
if (!isset($flags['config'])) $flags['config'] = '/home/laurent/src/notes/cli-client/.config';
$config = new Config($flags['config']);
$dataPath = '/home/laurent/src/notes/cli-client/test_' . $config->get('client_id');
$api = new Api('http://127.0.0.1:8000');
$session = $api->login('test@example.com', '12345678', $config->get('client_id'));
$api->setSessionId($session['id']);
if (array_key_exists('sync', $flags)) {
$syncStartTime = time();
$lastSyncTime = $config->get('last_sync_time');
$folderItems = new FolderItems();
$folderItems->fromPath($dataPath);
// ------------------------------------------------------------------------------------------
// Get latest changes from API
// ------------------------------------------------------------------------------------------
$response = $api->exec('GET', 'synchronizer', array('last_id' => $config->get('last_sync_id')));
// $response = $api->exec('GET', 'synchronizer', array('last_id' => 80));
$pathMap = array();
$folders = array();
$notes = array();
$maxId = null;
foreach ($response['items'] as $item) {
$folderItem = new FolderItem();
switch ($item['type']) {
case 'create':
case 'update':
$resource = $api->exec('GET', $item['item_type'] . 's/' . $item['item_id']);
$folderItem->fromApiArray($item['item_type'], $resource);
break;
default:
throw new Exception('Unsupported action type: ' . $item['type']);
}
$folderItems->setById($folderItem->id(), $folderItem);
$maxId = max($item['id'], $maxId);
}
foreach ($folderItems->all() as $item) {
$relativePath = $folderItems->itemFullPath($item);
$path = $dataPath . '/' . $relativePath;
foreach (array('folder', 'note') as $itemType) {
if ($item->isFolder() && $itemType == 'folder') {
@mkdir($path, 0755, true); // Ignore "File exists" warning
if (!is_dir($path)) throw new Exception('Could not create folder at ' . $path);
}
if ($item->isNote() && $itemType == 'note') {
if ($item->body() !== file_get_contents($path)) {
file_put_contents($path, $item->body());
}
}
}
$pathMap[$item->id()] = $relativePath;
}
// ------------------------------------------------------------------------------------------
// Send changed notes and folders to API
// ------------------------------------------------------------------------------------------
foreach ($folderItems->all() as $item) {
if ($item->modTime() < $lastSyncTime) continue;
if ($item->isFolder()) {
$api->exec('PUT', 'folders/' . $item->id(), null, $item->toApiArray());
} else {
$api->exec('PUT', 'notes/' . $item->id(), null, $item->toApiArray());
}
}
$config->set('last_sync_time', $syncStartTime);
$config->set('folder_items', json_encode($pathMap));
if ($maxId !== null) $config->set('last_sync_id', $maxId);
}

View File

@ -0,0 +1,3 @@
# FOLDER2/note1
Modified on client 2

View File

@ -0,0 +1 @@
# NOTE 1

View File

@ -0,0 +1 @@
# NOTE 2

View File

@ -0,0 +1 @@
test subnode

View File

@ -0,0 +1,3 @@
# FOLDER2/note1
Modified on client 2

View File

@ -0,0 +1 @@
# NOTE 1

View File

@ -0,0 +1 @@
# NOTE 2

View File

@ -0,0 +1 @@
test subnode

66
composer.json Normal file
View File

@ -0,0 +1,66 @@
{
"name": "laurent/notes",
"license": "proprietary",
"type": "project",
"autoload": {
"psr-4": {
"": "src/"
},
"classmap": [
"app/AppKernel.php",
"app/AppCache.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*",
"doctrine/orm": "^2.5",
"doctrine/doctrine-bundle": "^1.6",
"doctrine/doctrine-cache-bundle": "^1.2",
"symfony/swiftmailer-bundle": "^2.3",
"symfony/monolog-bundle": "^2.8",
"symfony/polyfill-apcu": "^1.0",
"sensio/distribution-bundle": "^5.0",
"sensio/framework-extra-bundle": "^3.0.2",
"incenteev/composer-parameter-handler": "^2.0",
"illuminate/database": "*",
"yetanotherape/diff-match-patch": "*"
},
"require-dev": {
"sensio/generator-bundle": "^3.0",
"symfony/phpunit-bridge": "^3.0"
},
"scripts": {
"symfony-scripts": [
"Incenteev\\ParameterHandler\\ScriptHandler::buildParameters",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile",
"Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget"
],
"post-install-cmd": [
"@symfony-scripts"
],
"post-update-cmd": [
"@symfony-scripts"
]
},
"extra": {
"symfony-app-dir": "app",
"symfony-bin-dir": "bin",
"symfony-var-dir": "var",
"symfony-web-dir": "web",
"symfony-tests-dir": "tests",
"symfony-assets-install": "relative",
"incenteev-parameters": {
"file": "app/config/parameters.yml"
}
}
}

2386
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

11
notes.sublime-project Normal file
View File

@ -0,0 +1,11 @@
{
"folders":
[
{
"path": ".",
"folder_exclude_patterns": [
"var"
]
}
]
}

31
phpunit.xml.dist Normal file
View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="app/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
<server name="KERNEL_DIR" value="app/" />
</php>
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
<exclude>
<directory>src/*Bundle/Resources</directory>
<directory>src/*/*Bundle/Resources</directory>
<directory>src/*/Bundle/*Bundle/Resources</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

2
run_tests.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
phpunit --bootstrap vendor/autoload.php tests/Model/

7
src/.htaccess Normal file
View File

@ -0,0 +1,7 @@
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order deny,allow
Deny from all
</IfModule>

View File

@ -0,0 +1,9 @@
<?php
namespace AppBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AppBundle extends Bundle
{
}

View File

@ -0,0 +1,197 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use AppBundle\Model\BaseModel;
use AppBundle\Model\Session;
use AppBundle\Model\User;
use AppBundle\Exception\ForbiddenException;
use AppBundle\Exception\UnauthorizedException;
use Illuminate\Database\Eloquent\Collection;
use AppBundle\Exception\BaseException;
abstract class ApiController extends Controller {
protected $db = null;
protected $session = null;
protected $user = null;
private $useTestUserAndSession = true;
private $testClientNum = 1;
public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container = null) {
parent::setContainer($container);
set_exception_handler(function($e) {
if ($e instanceof BaseException) {
$r = $e->toJsonResponse();
$r->send();
echo "\n";
} else {
$msg = array();
$msg[] = 'Exception: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine();
$msg[] = '';
$msg[] = $e->getTraceAsString();
echo implode("\n", $msg);
echo "\n";
}
});
// 1. client 1 : bla bla bla
// 2. client 2 : bla bla bla fromclient2
// 3. client 1 : client1bla bla bla
// RESULT: client1bla bla bla
// Because diff for 3 is done between 2 and 3
// Need to introduce revID so that Change class knows between which versions the diff should be made
// HACK: get connection once here so that it's initialized and can
// be accessed from models.
$this->db = $this->get('app.eloquent')->connection();
$s = $this->session();
// TODO: to keep it simple, only respond to logged in users, but in theory some data
// could be public.
if (!$s || !$this->user()) throw new UnauthorizedException('A session and user are required');
BaseModel::setClientId($s ? $s->client_id : 0);
}
protected function session() {
if ($this->useTestUserAndSession) {
$session = Session::find(Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'));
if ($session) $session->delete();
$session = new Session();
$session->id = Session::unhex('BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB');
$session->owner_id = Session::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$session->client_id = Session::unhex($this->testClientNum == 1 ? 'C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1C1' : 'C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2');
$session->save();
return $session;
}
if ($this->session) return $this->session;
$request = $this->container->get('request_stack')->getCurrentRequest();
$this->session = Session::find(BaseModel::unhex($request->query->get('session')));
return $this->session;
}
protected function user() {
if ($this->useTestUserAndSession) {
$user = User::find(User::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'));
if (!$user) {
$user = new User();
$user->id = User::unhex('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$user->owner_id = $user->id;
$user->email = 'test@example.com';
$user->password = Session::hashPassword('12345678');
$user->save();
}
return $user;
}
if ($this->user) return $this->user;
$s = $this->session();
$this->user = $s ? $s->owner() : null;
return $this->user;
}
protected function aclCheck($resource) {
if (!is_array($resource)) $resource = array($resource);
$user = $this->user();
if (!$user) throw new ForbiddenException();
foreach ($resource as $r) {
if (!isset($r->owner_id)) continue;
if ($r->owner_id != $user->id) throw new ForbiddenException();
}
}
static private function serializeResponse($data) {
$output = $data;
if ($output instanceof Collection) $output = $output->all();
if ($output instanceof BaseModel) {
$output = $output->toPublicArray();
} else if (is_array($output)) {
foreach ($output as $k => $v) {
$output[$k] = self::serializeResponse($v);
}
}
return $output;
}
static protected function successResponse($data = null) {
$output = self::serializeResponse($data);
return new JsonResponse($output);
}
static protected function errorResponse($message, $errorCode = 0, $httpCode = 400) {
$o = array('error' => $message, 'code' => $errorCode);
$response = new JsonResponse($o);
$response->setStatusCode($httpCode);
return $response;
}
protected function multipleValues($v) {
if ($v === null || $v === false) return array();
if (strpos((string)$v, ';') === false) return array($v);
return explode(';', $v);
}
// PHP doesn't parse PATCH and PUT requests automatically, so it needs
// to be done manually.
// http://stackoverflow.com/a/5488449/561309
protected function patchParameters() {
$output = array();
$input = file_get_contents('php://input');
preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);
$boundary = $matches[1];
$blocks = preg_split("/-+$boundary/", $input);
array_pop($blocks);
foreach ($blocks as $id => $block) {
if (empty($block)) continue;
// you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char
// parse uploaded files
if (strpos($block, 'application/octet-stream') !== FALSE) {
// match "name", then everything after "stream" (optional) except for prepending newlines
preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches);
} else {
// match "name" and optional value in between newline sequences
preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);
}
if (!isset($matches[2])) {
// Regex above will not find anything if the parameter has not value. For example
// "parent_id" below:
// Content-Disposition: form-data; name="parent_id"
//
//
// Content-Disposition: form-data; name="id"
//
// 54ad197be333c98778c7d6f49506efcb
$output[$matches[1]] = '';
} else {
$output[$matches[1]] = $matches[2];
}
}
return $output;
}
protected function putParameters() {
return $this->patchParameters();
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController;
use AppBundle\Model\Folder;
class FoldersController extends ApiController {
/**
* @Route("/folders")
*/
public function allAction(Request $request) {
if ($request->isMethod('POST')) {
$folder = new Folder();
$folder->fromPublicArray($request->request->all());
$folder->save();
return static::successResponse($folder);
}
return static::errorResponse('Invalid method');
}
/**
* @Route("/folders/{id}")
*/
public function oneAction($id, Request $request) {
$folder = Folder::find(Folder::unhex($id));
if (!$folder && !$request->isMethod('PUT')) return static::errorResponse('Not found', 0, 404);
if ($request->isMethod('GET')) {
return static::successResponse($folder);
}
if ($request->isMethod('PUT')) {
// TODO: call fromPublicArray() - handles unhex conversion
$data = $this->putParameters();
$isNew = !$folder;
if ($isNew) $folder = new Folder();
foreach ($data as $n => $v) {
if ($n == 'parent_id') $v = Folder::unhex($v);
$folder->{$n} = $v;
}
$folder->owner_id = $this->user()->id;
$folder->id = Folder::unhex($id);
$folder->setIsNew($isNew);
$folder->save();
return static::successResponse($folder);
}
return static::successResponse($folder);
}
/**
* @Route("/folders/{id}/notes")
*/
public function linkAction($id, Request $request) {
$folder = Folder::find(Folder::unhex($id));
if (!$folder) return static::errorResponse('Not found', 0, 404);
if ($request->isMethod('GET')) {
return static::successResponse($folder->notes());
}
if ($request->isMethod('POST')) {
$ids = $this->multipleValues($request->request->get('id'));
if (!count($ids)) static::errorResponse('id parameter is missing');
$ids = Folder::unhex($ids);
$folder->add($ids);
return static::successResponse();
}
return static::errorResponse('Invalid method');
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController;
use AppBundle\Model\Note;
class NotesController extends ApiController {
/**
* @Route("/notes")
*/
public function allAction(Request $request) {
if ($request->isMethod('POST')) {
$note = new Note();
$note->fromPublicArray($request->request->all());
$note->owner_id = $this->user()->id;
$note->save();
return static::successResponse($note->toPublicArray());
}
return static::errorResponse('Invalid method');
}
/**
* @Route("/notes/{id}")
*/
public function oneAction($id, Request $request) {
$note = Note::find(Note::unhex($id));
if (!$note && !$request->isMethod('PUT')) return static::errorResponse('Not found', 0, 404);
if ($request->isMethod('GET')) {
return static::successResponse($note);
}
if ($request->isMethod('PUT')) {
$data = $this->putParameters();
$isNew = !$note;
if ($isNew) $note = new Note();
foreach ($data as $n => $v) {
if ($n == 'parent_id') $v = Note::unhex($v);
$note->{$n} = $v;
}
$note->id = Note::unhex($id);
$note->owner_id = $this->user()->id;
$note->setIsNew($isNew);
$note->save();
return static::successResponse($note);
}
if ($request->isMethod('PATCH')) {
$data = $this->patchParameters();
foreach ($data as $n => $v) {
$note->{$n} = $v;
}
$note->save();
return static::successResponse($note);
}
if ($request->isMethod('DELETE')) {
$note->delete();
return static::successResponse();
}
return static::errorResponse('Invalid method');
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController;
use AppBundle\Model\User;
use AppBundle\Model\Session;
use AppBundle\Exception\NotFoundException;
use AppBundle\Exception\MethodNotAllowedException;
use AppBundle\Model\Action;
class SessionsController extends ApiController {
/**
* @Route("/sessions")
*/
public function allAction(Request $request) {
if ($request->isMethod('POST')) {
$data = $request->request->all();
// Note: the login method will throw an exception in case of failure
$session = Session::login($data['email'], $data['password'], Session::unhex($data['client_id']));
return static::successResponse($session);
}
throw new MethodNotAllowedException();
}
/**
* @Route("/sessions/{id}", name="one_session")
*/
public function oneAction($id, Request $request) {
$session = Session::find(Session::unhex($id));
if (!$session) throw new NotFoundException();
return static::successResponse($session);
}
}

View File

@ -0,0 +1,114 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController;
use AppBundle\Model\Action;
use AppBundle\Exception\UnauthorizedException;
/*
JS
class Session
::login()
class Synchronizer
::fromId(id)
class Note
class User
HISTORY
client_id, create, type, id => get full object from api
update, type, id => get full object from api
delete, type, id => remove object
Revisions
----------------
id
client_id
(user_id ?)
action (create, update, delete)
item_type (note, folder)
item_field (title, body, completed...)
item_id
if current client ID = revision.client_id - skip
Current client id = 123
Client ID | Action | Item ID
------------------------------------
456 delete 777
123 update 777 - conflict - move to folder - note that deleted by X and then modified by Y
Client ID | Action | Item ID | Rev ID
------------------------------------------------------
456 update 777 | 2
123 update 777 - conflict | 3
Find rev 1 and do three way merge with 2 and 3 - means there's a need to store history of all versions of body/title
// Each item should have a revision ID.
// When processing revisions => if item revision = revision.id - skip
API CALL
/synchronizer/?last_id=<last_id_that_caller_synched_to>
SIMPLE IMPLEMENTATION:
loop through changes
create list of actions:
create one action per item ID / field (by merging all into one)
loop through actions
skip current client id = action.client_id
send back list:
{
sync_id: 132456,
notes: [
{
action: 'update',
field: 'body',
body: 'blabla'
}
],
has_more: true
}
If has_more, resume with last sync_id
*/
class SynchronizerController extends ApiController {
/**
* @Route("/synchronizer")
*/
public function allAction(Request $request) {
$id = (int)$request->query->get('last_id');
if (!$this->user() || !$this->session()) throw new UnauthorizedException();
$actions = Action::actionsDoneAfterId($this->user()->id, $this->session()->client_id, $id);
return static::successResponse($actions);
}
}

View File

@ -0,0 +1,209 @@
<?php
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use AppBundle\Controller\ApiController;
use AppBundle\Model\User;
use AppBundle\Model\Session;
use AppBundle\Model\Change;
use AppBundle\Model\FolderItem;
use AppBundle\Exception\ValidationException;
use AppBundle\Diff;
use DiffMatchPatch\DiffMatchPatch;
class UsersController extends ApiController {
/**
* @Route("/users")
*/
public function allAction(Request $request) {
$source = "This is the first line.\n\nThis is the second line.";
$target1 = "This is the first line XXX.\n\nThis is the second line.";
$target2 = "This is the first line.\n\nThis is the second line YYY.";
$r = Diff::merge3($source, $target1, $target2);
var_dump($r);die();
// $dmp = new DiffMatchPatch();
// $patches = $dmp->patch_make($source, $target1);
// // @@ -1,11 +1,12 @@
// // Th
// // -e
// // +at
// // quick b
// // @@ -22,18 +22,17 @@
// // jump
// // -s
// // +ed
// // over
// // -the
// // +a
// // laz
// $result = $dmp->patch_apply($patches, $target2);
// var_dump($result);
// die();
// $dmp = new DiffMatchPatch();
// $source = "This is the first line.\n\nThis is the second line.";
// $target1 = "This is the first line XXX.\n\nThis is the second line.";
// $target2 = "edsùfrklq lkzerlmk zemlkrmzlkerm lze.";
// $diff1 = $dmp->patch_make($source, $target1);
// $diff2 = $dmp->patch_make($source, $target2);
// //var_dump($dmp->patch_toText($diff1));
// // //var_dump($diff1[0]->patch_toText());
// $r = $dmp->patch_apply($diff1, $source);
// $r = $dmp->patch_apply($diff1, $target2);
// var_dump($r);die();
// $r = $dmp->patch_apply($diff2, $r[0]);
// var_dump($r);
// $dmp = new DiffMatchPatch();
// $patches = $dmp->patch_make($source, $target1);
// // @@ -1,11 +1,12 @@
// // Th
// // -e
// // +at
// // quick b
// // @@ -22,18 +22,17 @@
// // jump
// // -s
// // +ed
// // over
// // -the
// // +a
// // laz
// $result = $dmp->patch_apply($patches, $target2);
// var_dump($result);
// die();
// $r = Diff::merge($source, $target1, $target2);
// var_dump($r);die();
// $diff1 = xdiff_string_diff($source, $target1);
// $diff2 = xdiff_string_diff($source, $target2);
// $errors = array();
// $t = xdiff_string_merge3($source , $target1, $target2, $errors);
// var_dump($errors);
// var_dump($t);die();
// var_dump($diff1);
// var_dump($diff2);
// $errors = array();
// $t = xdiff_string_patch($source, $diff1, XDIFF_PATCH_NORMAL, $errors);
// var_dump($t);
// var_dump($errors);
// $errors = array();
// $t = xdiff_string_patch($t, $diff2, XDIFF_PATCH_NORMAL, $errors);
// var_dump($t);
// var_dump($errors);
// var_dump($diff1);
// var_dump($diff2);
// $change = new Change();
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
// $change->item_type = FolderItem::enumId('type', 'note');
// $change->item_field = FolderItem::enumId('field', 'title');
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->delta = 'salut ca va';
// $change->save();
// $change = new Change();
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
// $change->item_type = FolderItem::enumId('type', 'note');
// $change->item_field = FolderItem::enumId('field', 'title');
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut, ça va ? oui très bien');
// $change->save();
// $change = new Change();
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
// $change->item_type = FolderItem::enumId('type', 'note');
// $change->item_field = FolderItem::enumId('field', 'title');
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut - oui très bien');
// $change->save();
// $change = new Change();
// $change->user_id = FolderItem::unhex('204705F2E2E698036034FDC709840B80');
// $change->client_id = FolderItem::unhex('11111111111111111111111111111111');
// $change->item_type = FolderItem::enumId('type', 'note');
// $change->item_field = FolderItem::enumId('field', 'title');
// $change->item_id = FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD');
// $change->createDelta('salut, ça va ? oui bien');
// $change->save();
$d = Change::fullFieldText(FolderItem::unhex('DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD'), FolderItem::enumId('field', 'title'));
var_dump($d);die();
die();
// $fineDiff = $this->get('app.fine_diff');
// $opcodes = $fineDiff->getDiffOpcodes('salut ca va', 'salut va?');
// var_dump($opcodes);
// $merged = $fineDiff->renderToTextFromOpcodes('salut ca va', $opcodes);
// var_dump($merged);
// die();
if ($request->isMethod('POST')) {
$user = new User();
$data = $request->request->all();
$errors = User::validate($data);
if (count($errors)) throw ValidationException::fromErrors($errors);
$data['password'] = Session::hashPassword($data['password']);
$user->fromPublicArray($data);
$user->save();
return static::successResponse($user->toPublicArray());
}
return static::errorResponse('Invalid method');
}
/**
* @Route("/users/{id}")
*/
public function oneAction($id, Request $request) {
$user = User::find(User::unhex($id));
if (!$user) return static::errorResponse('Not found', 0, 404);
$this->aclCheck($user);
return static::successResponse($user);
}
}

33
src/AppBundle/Diff.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace AppBundle;
use DiffMatchPatch\DiffMatchPatch;
class Diff {
static private $dmp_ = null;
static private function dmp() {
if (self::$dmp_) return self::$dmp_;
self::$dmp_ = new DiffMatchPatch();
return self::$dmp_;
}
static public function diff($from, $to) {
return self::dmp()->patch_toText(self::dmp()->patch_make($from, $to));
}
static public function patch($from, $diff) {
return self::dmp()->patch_apply(self::dmp()->patch_fromText($diff), $from);
}
// Basically applies diff(orginal, mod1) to mod2. Note sure if this is really
// equivalent to a three-way merge.
static public function merge3($original, $modified1, $modified2) {
$patches = self::dmp()->patch_make($original, $modified1);
return self::dmp()->patch_apply($patches, $modified2);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace AppBundle;
class Eloquent {
private $capsule_ = null;
public function __construct() {
$this->capsule_ = new \Illuminate\Database\Capsule\Manager();
$this->capsule_->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'notes',
'username' => 'root',
'password' => 'pass',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$this->capsule_->bootEloquent();
}
public function connection() {
return $this->capsule_->getConnection('default');
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace AppBundle\Exception;
class AuthException extends BaseException {
protected $message = 'invalid authentication';
}

View File

@ -0,0 +1,36 @@
<?php
namespace AppBundle\Exception;
use Symfony\Component\HttpFoundation\JsonResponse;
class BaseException extends \Exception {
protected $httpStatusCode = 400;
public function getType() {
$t = str_replace('Exception', '', get_called_class());
$t = str_replace("AppBundle\\\\", '', $t);
return $t;
}
public function getHttpStatusCode() {
return $this->httpStatusCode;
}
public function toErrorArray() {
$o = array();
$o['error'] = $this->getMessage();
if ($this->getCode()) $o['code'] = $this->getCode();
$o['type'] = $this->getType();
return $o;
}
public function toJsonResponse($errorObject = null) {
if (!$errorObject) $errorObject = $this->toErrorArray();
$response = new JsonResponse($errorObject);
$response->setStatusCode($this->getHttpStatusCode());
return $response;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace AppBundle\Exception;
class ForbiddenException extends BaseException {
protected $message = 'forbidden';
protected $httpStatusCode = 403;
}

View File

@ -0,0 +1,10 @@
<?php
namespace AppBundle\Exception;
class MethodNotAllowedException extends BaseException {
protected $message = 'method not allowed';
protected $httpStatusCode = 405;
}

View File

@ -0,0 +1,10 @@
<?php
namespace AppBundle\Exception;
class NotFoundException extends BaseException {
protected $message = 'not found';
protected $httpStatusCode = 400;
}

View File

@ -0,0 +1,10 @@
<?php
namespace AppBundle\Exception;
class UnauthorizedException extends BaseException {
protected $message = 'unauthorized';
protected $httpStatusCode = 401;
}

View File

@ -0,0 +1,23 @@
<?php
namespace AppBundle\Exception;
class ValidationException extends BaseException {
protected $message = 'validation error';
public $validationErrors = array();
static public function fromErrors($errors) {
if (!count($errors)) return new ValidationException();
$e = new ValidationException($errors[0]['message']);
$e->validationErrors = $errors;
return $e;
}
public function toErrorArray() {
$o = parent::toErrorArray();
$o['validation_errors'] = $this->validationErrors;
return $o;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace AppBundle\Model;
class Action extends BaseModel {
static protected $enums = array(
'type' => array('create', 'update', 'delete'),
);
static public function actionsDoneAfterId($userId, $clientId, $actionId) {
$limit = 100;
$items = self::where('id', '>', $actionId)
->where('user_id', '=', $userId)
->where('client_id', '!=', $clientId)
->orderBy('id')
->limit($limit + 1)
->get();
$hasMore = $limit < count($items);
if ($hasMore) array_pop($items);
return array(
'has_more' => $hasMore,
'items' => $items,
);
}
}

View File

@ -0,0 +1,286 @@
<?php
namespace AppBundle\Model;
use \Illuminate\Database\Eloquent\Model;
use \Illuminate\Support\Facades\DB;
class BaseModel extends \Illuminate\Database\Eloquent\Model {
public $timestamps = false;
public $useUuid = false;
protected $changedVersionedFieldValues = array();
protected $versionedFields = array();
private $isNew = null;
private $revId = 0;
static private $clientId = null;
static protected $enums = array(
'field' => array('title', 'body', 'completed'),
);
static protected $defaultValidationRules = array();
static protected $defaultValidationMessages = array(
'required' => '{key} is required',
'notEmpty' => '{key} cannot be empty',
'minLength' => '{key} must be at least {arg0} characters long',
'maxLength' => '{key} must not be longer than {arg0} characters',
'function' => '{key} is invalid',
);
public function __construct($attributes = array()) {
parent::__construct($attributes);
}
static public function setClientId($clientId) {
self::$clientId = $clientId;
}
static public function clientId() {
return self::$clientId;
}
public function fromPublicArray($array) {
foreach ($array as $k => $v) {
if ($k == 'rev_id') {
$this->revId = $v;
} else if (in_array($k, $this->versionedFields)) {
$this->changedVersionedFieldValues[$k] = $v;
} else {
$this->{$k} = $v;
}
}
}
public function toPublicArray() {
$output = $this->toArray();
if ($this->useUuid) {
$output['id'] = self::hex($output['id']);
}
if (!empty($output['parent_id'])) $output['parent_id'] = self::hex($output['parent_id']);
if (!empty($output['owner_id'])) $output['owner_id'] = self::hex($output['owner_id']);
if (!empty($output['client_id'])) $output['client_id'] = self::hex($output['client_id']);
if (!empty($output['item_id'])) $output['item_id'] = self::hex($output['item_id']);
if (!empty($output['user_id'])) $output['user_id'] = self::hex($output['user_id']);
foreach ($output as $k => $v) {
if (isset(static::$enums[$k])) {
$output[$k] = static::enumName($k, $v);
}
}
if (isset($output['item_type'])) {
$output['item_type'] = FolderItem::enumName('type', $output['item_type'], true);
}
if (isset($output['item_field'])) {
$output['item_field'] = BaseModel::enumName('field', $output['item_field'], true);
}
$maxRevId = 0;
foreach ($this->versionedFields as $field) {
$r = $this->versionedFieldValue($field, true);
$output[$field] = $r['text'];
$maxRevId = max($maxRevId, $r['revId']);
}
$output['rev_id'] = $maxRevId;
return $output;
}
public function versionedFieldValue($fieldName, $returnRevId = false) {
return Change::fullFieldText($this->id, BaseModel::enumId('field', $fieldName), null, $returnRevId);
}
public function setVersionedFieldValue($fieldName, $fieldValue) {
$this->changedVersionedFieldValues[$fieldName] = $fieldValue;
}
static public function createId() {
return openssl_random_pseudo_bytes(16);
}
static public function hex($id) {
if (is_array($id)) {
foreach ($id as $k => $v) {
$id[$k] = self::hex($v);
}
return $id;
}
return bin2hex($id);
}
static public function unhex($s) {
if (!strlen($s)) return null;
if (is_array($s)) {
foreach ($s as $k => $v) {
$s[$k] = self::unhex($v);
}
return $s;
}
$output = @hex2bin($s);
if ($output === false) return null;
return $output;
}
public function owner() {
if (!isset($this->owner_id)) return null;
return User::find($this->owner_id);
}
static public function validate($data, $rules = null) {
if (!$rules) $rules = static::$defaultValidationRules;
$errors = array();
foreach ($rules as $key => $keyRules) {
foreach ($keyRules as $rule) {
$ok = true;
switch ($rule['type']) {
case 'required':
if (!array_key_exists($key, $data)) $ok = false;
break;
case 'notEmpty':
if (array_key_exists($key, $data) && !strlen((string)$data[$key])) $ok = false;
break;
case 'minLength':
if (array_key_exists($key, $data) && strlen((string)$data[$key]) < $rule['args'][0]) $ok = false;
break;
case 'maxLength':
if (array_key_exists($key, $data) && strlen((string)$data[$key]) > $rule['args'][0]) $ok = false;
break;
case 'function':
$ok = call_user_func_array($rule['args'][0], array($key, $rule, $data));
break;
default:
throw new \Exception(sprintf('unsupported validation rule: "%s"', $rule['name']));
}
if (!$ok) {
$errors[] = array(
'key' => $key,
'type' => $rule['type'] == 'function' ? 'other' : $rule['type'],
'message' => static::validationMessage($key, $rule, $data),
);
}
}
}
return $errors;
}
static public function validationMessage($key, $rule, $data) {
$msg = static::$defaultValidationMessages[$rule['type']];
if (isset($rule['message'])) $msg = $rule['message'];
$msg = str_replace('{key}', $key, $msg);
$msg = str_replace('{value}', isset($data[$key]) ? $data[$key] : '', $msg);
$args = isset($rule['args']) ? $rule['args'] : array();
for ($i = 0; $i < count($args); $i++) {
$v = $args[$i];
if (is_array($v)) $v = '';
if (is_object($v) && !method_exists($v, '__toString')) $v = '';
$v = (string)$v;
$msg = str_replace(sprintf('{arg%s}', $i), $v, $msg);
}
return $msg;
}
static public function enumName($enumType, $enumId, $returnNullOnError = false) {
foreach (static::$enums[$enumType] as $index => $name) {
if ($index + 1 == $enumId) return $name;
}
if ($returnNullOnError) return null;
throw new \Exception(sprintf('Invalid enum: %s/%s', $enumType, $enumId));
}
static public function enumId($enumType, $enumName, $returnNullOnError = false) {
if (!isset(static::$enums[$enumType])) throw new \Exception(sprintf('Invalid enum type: %s', $enumType));
foreach (static::$enums[$enumType] as $index => $name) {
if ($name == $enumName) return $index + 1;
}
if ($returnNullOnError) return null;
throw new \Exception(sprintf('Invalid enum: %s/%s', $enumType, $enumName));
}
// Allows caller to force the model to be detected as new,
// even when the model already has an ID (to handle PUT
// calls that manually set the ID).
public function setIsNew($v) {
$this->isNew = $v;
}
public function save(Array $options = array()) {
$isNew = !$this->id || $this->isNew === true;
if ($this->useUuid && $isNew && !$this->id) $this->id = self::createId();
$this->updated_time = time(); // TODO: maybe only update if one of the fields, or if some of versioned data has changed
if ($isNew) $this->created_time = time();
parent::save($options);
if (count($this->versionedFields)) {
$this->recordChanges($isNew ? 'create' : 'update', $this->changedVersionedFieldValues);
}
$this->changedVersionedFieldValues = array();
}
public function delete() {
parent::delete();
if (count($this->versionedFields)) {
$this->recordChanges('delete');
}
}
protected function recordChanges($type, $versionedData = array()) {
if ($type == 'delete') {
$change = $this->newChange($type);
$change->save();
} else if ($type == 'create' || $type == 'update') {
foreach ($this->versionedFields as $field) {
if (!isset($versionedData[$field])) continue;
$change = $this->newChange($type);
$change->item_field = BaseModel::enumId('field', $field);
$change->createDelta($versionedData[$field]);
$change->save();
}
} else {
throw new \Exception('Unknown type: ' . $type);
}
}
private function classItemTypeName() {
$s = strtolower(get_called_class());
$s = explode("\\", $s);
return $s[count($s) - 1];
}
private function newChange($type) {
if (static::clientId() === null) throw new \Exception('Client ID must be specified');
$change = new Change();
$change->user_id = $this->owner_id;
$change->client_id = static::clientId();
$change->item_type = FolderItem::enumId('type', $this->classItemTypeName());
$change->type = Change::enumId('type', $type);
$change->item_id = $this->id;
return $change;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace AppBundle\Model;
use AppBundle\Diff;
class Change extends BaseModel {
static protected $enums = array(
'type' => array('create', 'update', 'delete'),
);
static public function changesDoneAfterId($userId, $clientId, $changeId) {
$limit = 100;
$items = self::where('id', '>', $changeId)
->where('user_id', '=', $userId)
->where('client_id', '!=', $clientId)
->orderBy('id')
->limit($limit + 1)
->get();
$hasMore = $limit < count($items);
if ($hasMore) array_pop($items);
return array(
'has_more' => $hasMore,
'items' => $items,
);
}
static public function itemFieldHistory($itemId, $itemField, $toId = null) {
$query = self::where('item_id', '=', $itemId);
$query->where('item_field', '=', $itemField);
if ($toId) $query->where('id', '<=', $toId);
$query->orderBy('id');
return $query->get();
}
static public function fullFieldText($itemId, $itemField, $toId = null, $returnRevId = false) {
$output = '';
$changes = self::itemFieldHistory($itemId, $itemField, $toId);
$revId = 0;
for ($i = 0; $i < count($changes); $i++) {
$change = $changes[$i];
$result = Diff::patch($output, $change->delta);
if (!count($result[1])) throw new \Exception('Unexpected result format for patch operation: ' . json_encode($result));
if (!$result[1][0]) {
// Could not patch the string. TODO: handle conflict
}
$output = $result[0];
$revId = $change->id;
}
return $returnRevId ? array('text' => $output, 'revId' => $revId) : $output;
}
public function createDelta($newText) {
$currentText = self::fullFieldText($this->item_id, $this->item_field, $this->previous_id);
$this->delta = Diff::diff($currentText, $newText);
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace AppBundle\Model;
class Folder extends FolderItem {
protected $versionedFields = array('title');
public function add($ids) {
$notes = Note::find($ids);
foreach ($notes as $note) {
$note->parent_id = $this->id;
$note->save();
}
}
public function notes() {
return Note::where('parent_id', '=', $this->id)->get();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace AppBundle\Model;
class FolderItem extends BaseModel {
public $useUuid = true;
public $incrementing = false;
static protected $enums = array(
'type' => array('folder', 'note', 'todo'),
);
}

View File

@ -0,0 +1,9 @@
<?php
namespace AppBundle\Model;
class Note extends FolderItem {
protected $versionedFields = array('title', 'body');
}

View File

@ -0,0 +1,40 @@
<?php
namespace AppBundle\Model;
use AppBundle\Exception\NotFoundException;
use AppBundle\Exception\AuthException;
class Session extends BaseModel {
public $useUuid = true;
public $incrementing = false;
static public function hashPassword($password) {
return password_hash($password, PASSWORD_DEFAULT);
}
static public function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
static public function passwordMinLength() {
return 8;
}
static public function login($email, $password, $clientId) {
$user = User::byEmail($email);
if (!$user) throw new NotFoundException();
$ok = self::verifyPassword($password, $user->password);
if (!$ok) throw new AuthException();
$session = new Session();
$session->owner_id = $user->id;
$session->client_id = $clientId;
$session->save();
return $session;
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace AppBundle\Model;
use AppBundle\Exception\ValidationException;
class User extends BaseModel {
public $useUuid = true;
public $incrementing = false;
public function __construct($attributes = array()) {
parent::__construct($attributes);
static::$defaultValidationRules['email'] = array(
array('type' => 'required'),
array('type' => 'notEmpty'),
array('type' => 'function', 'args' => array(array('AppBundle\Model\User', 'validateUniqueEmail')), 'message' => 'email "{value}" is already in use'),
);
static::$defaultValidationRules['password'] = array(
array('type' => 'required'),
array('type' => 'minLength', 'args' => array(8)),
);
}
public function toPublicArray() {
$output = parent::toPublicArray();
unset($output['password']);
return $output;
}
public function byEmail($email) {
return self::where('email', '=', $email)->first();
}
public function save(Array $options = array()) {
$isNew = !$this->id;
parent::save($options);
if ($isNew) {
$this->owner_id = $this->id;
parent::save($options);
}
}
static public function validateUniqueEmail($key, $rule, $data) {
if (!isset($data['email'])) return true;
$u = self::byEmail($data['email']);
return !$u;
}
}

57
structure.sql Normal file
View File

@ -0,0 +1,57 @@
CREATE TABLE `folders` (
`id` binary(16) NOT NULL,
`created_time` int(11) NOT NULL default '0',
`updated_time` int(11) NOT NULL default '0',
`parent_id` binary(16) NULL default NULL,
`owner_id` binary(16) NULL default NULL,
`is_encrypted` tinyint(1) NOT NULL default '0',
`encryption_method` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;
CREATE TABLE `notes` (
`id` binary(16) NOT NULL,
`completed` tinyint(1) NOT NULL default '0',
`created_time` int(11) NOT NULL default '0',
`updated_time` int(11) NOT NULL default '0',
`parent_id` binary(16) NULL default NULL,
`owner_id` binary(16),
`is_encrypted` tinyint(1) NOT NULL default '0',
`encryption_method` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;
CREATE TABLE `users` (
`id` binary(16) NOT NULL,
`email` varchar(256) NOT NULL default '',
`password` varchar(256) NOT NULL default '',
`validated` tinyint(1) NOT NULL default '0',
`created_time` int(11) NOT NULL default '0',
`updated_time` int(11) NOT NULL default '0',
`owner_id` binary(16),
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;
CREATE TABLE `sessions` (
`id` binary(16) NOT NULL,
`owner_id` binary(16),
`client_id` binary(16),
`created_time` int(11) NOT NULL default '0',
`updated_time` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;
CREATE TABLE `changes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` binary(16),
`client_id` binary(16),
`created_time` int(11) NOT NULL default '0',
`updated_time` int(11) NOT NULL default '0',
`type` int(11) NOT NULL default '0',
`item_id` binary(16),
`item_type` int(11) NOT NULL default '0',
`item_field` int(11) NOT NULL default '0',
`delta` MEDIUMTEXT,
`previous_id` int(11) NOT NULL default '0',
PRIMARY KEY (`id`)
) CHARACTER SET=utf8;

69
tests/BaseTestCase.php Normal file
View File

@ -0,0 +1,69 @@
<?php
use PHPUnit\Framework\TestCase;
use AppBundle\Model\BaseModel;
use AppBundle\Model\User;
use AppBundle\Model\Session;
class BaseTestCase extends TestCase {
protected function createModelId($type, $num = 1) {
$c = '';
if ($type == 'user') {
$c = 'A';
} else if ($type == 'client') {
$c = 'C';
} else if ($type == 'session') {
$c = 'B';
} else if ($type == 'note') {
$c = 'D';
}
return BaseModel::unhex(str_repeat($c . $num, 16));
}
protected function clientId($num = 1) {
return $this->createModelId('client', $num);
}
protected function user($num = 1) {
$id = $this->createModelId('user', $num);
$user = User::find($id);
if ($user) return $user;
$user = new User();
$user->id = $id;
$user->owner_id = $user->id;
$user->email = BaseModel::hex($id) . '@example.com';
$user->password = '$2y$10$YJeArRNypSbmpWG3RA83n.o78EVlyyVCFN71lWJ7.Omc1VEdwmX5W'; // Session::hashPassword('12345678');
$user->save();
return $user;
}
protected function session($userNum = 1, $clientNum = 1, $sessionNum = 1) {
$userId = $this->createModelId('user', $userNum);
$clientId = $this->createModelId('client', $clientNum);
$sessionId = $this->createModelId('session', $sessioNum);
$session = Session::find($sessionId);
if ($session) return $session;
$session = new Session();
$session->id = $sessionId;
$session->owner_id = $userId;
$session->client_id = $clientId;
$session->save();
return $session;
}
public function setUp() {
BaseModel::setClientId($this->clientId());
}
public function tearDown() {
}
}

132
tests/Model/ChangeTest.php Normal file
View File

@ -0,0 +1,132 @@
<?php
require_once dirname(dirname(__FILE__)) . '/setup.php';
use AppBundle\Model\BaseModel;
use AppBundle\Model\FolderItem;
use AppBundle\Model\Note;
use AppBundle\Model\Change;
class ChangeTest extends BaseTestCase {
public function setUp() {
parent::setUp();
Change::truncate();
Note::truncate();
}
public function tearDown() {
}
public function testDiff() {
$text1 = 'abcd efgh ijkl';
$itemId = $this->createModelId('note');
$change = new Change();
$change->user_id = $this->user()->id;
$change->client_id = $this->clientId();
$change->item_type = FolderItem::enumId('type', 'note');
$change->item_field = BaseModel::enumId('field', 'body');
$change->type = Change::enumId('type', 'create');
$change->item_id = $itemId;
$change->createDelta($text1);
$change->save();
$text2 = 'cd efgh NEW ijkl';
$change = new Change();
$change->user_id = $this->user()->id;
$change->client_id = $this->clientId();
$change->item_type = FolderItem::enumId('type', 'note');
$change->item_field = BaseModel::enumId('field', 'body');
$change->type = Change::enumId('type', 'update');
$change->item_id = $itemId;
$change->createDelta($text2);
$change->save();
$r = Change::fullFieldText($itemId, BaseModel::enumId('field', 'body'));
$this->assertEquals($r, $text2);
}
public function testDiff3Ways() {
// Scenario where two different clients change the same note at the same time.
//
// Client 1: 'abcd efgh ijkl' => 'cd efgh ijkl FROMCLIENT2'
// Client 2: 'abcd efgh ijkl' => 'abcd CLIENT1 efgh ijkl'
// Expected: 'cd CLIENT1 efgh ijkl FROMCLIENT2'
$text1 = 'abcd efgh ijkl';
$itemId = $this->createModelId('note');
$change = new Change();
$change->user_id = $this->user()->id;
$change->client_id = $this->clientId(1);
$change->item_type = FolderItem::enumId('type', 'note');
$change->item_field = BaseModel::enumId('field', 'body');
$change->type = Change::enumId('type', 'create');
$change->item_id = $itemId;
$change->createDelta($text1);
$change->save();
$changeId1 = $change->id;
$text2 = 'cd efgh ijkl FROMCLIENT2';
$change = new Change();
$change->user_id = $this->user()->id;
$change->client_id = $this->clientId(2);
$change->item_type = FolderItem::enumId('type', 'note');
$change->item_field = BaseModel::enumId('field', 'body');
$change->type = Change::enumId('type', 'update');
$change->item_id = $itemId;
$change->previous_id = $changeId1;
$change->createDelta($text2);
$change->save();
$changeId2 = $change->id;
$text3 = 'abcd CLIENT1 efgh ijkl';
$change = new Change();
$change->user_id = $this->user()->id;
$change->client_id = $this->clientId(1);
$change->item_type = FolderItem::enumId('type', 'note');
$change->item_field = BaseModel::enumId('field', 'body');
$change->type = Change::enumId('type', 'update');
$change->item_id = $itemId;
$change->previous_id = $changeId1;
$change->createDelta($text3);
$change->save();
$changeId3 = $change->id;
$r = Change::fullFieldText($itemId, BaseModel::enumId('field', 'body'));
$this->assertEquals($r, 'cd CLIENT1 efgh ijkl FROMCLIENT2');
}
public function testRevId() {
$n = new Note();
$n->setVersionedFieldValue('body', 'abcd efgh');
$n->save();
$noteId = $n->id;
$n = Note::find($noteId);
$d = $n->toPublicArray();
$this->assertEquals(1, $d['rev_id']);
$n->setVersionedFieldValue('body', '123456');
$n->save();
$n = Note::find($noteId);
$d = $n->toPublicArray();
$this->assertEquals(2, $d['rev_id']);
}
}

42
tests/Model/NoteTest.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require_once dirname(dirname(__FILE__)) . '/setup.php';
use AppBundle\Model\Note;
class NoteTest extends BaseTestCase {
public function setUp() {
parent::setUp();
}
public function testCanSaveAndLoad() {
$note = new Note();
$note->setVersionedFieldValue('title', 'the title');
$note->setVersionedFieldValue('body', 'the body');
$note->save();
$note = $note->find($note->id);
$this->assertNotNull($note);
$this->assertEquals('the title', $note->versionedFieldValue('title'));
$this->assertEquals('the body', $note->versionedFieldValue('body'));
}
public function testFromToPublicArray() {
$a = array(
'title' => 'the title',
'body' => 'the body',
);
$note = new Note();
$note->fromPublicArray($a);
$note->save();
$note = $note::find($note->id);
$b = $note->toPublicArray();
$this->assertEquals('the title', $b['title']);
$this->assertEquals('the body', $b['body']);
$this->assertArrayHasKey('rev_id', $b);
}
}

27
tests/setup.php Normal file
View File

@ -0,0 +1,27 @@
<?php
require_once dirname(__FILE__) . '/BaseTestCase.php';
$dbName = 'notes_test';
$structureFile = dirname(dirname(__FILE__)) . '/structure.sql';
$cmd = sprintf("mysql -u root -ppass -e 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;'", $dbName, $dbName);
exec($cmd);
$cmd = sprintf("mysql -u root -ppass %s < '%s'", $dbName, $structureFile);
exec($cmd);
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'notes_test',
'username' => 'root',
'password' => 'pass',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$capsule->bootEloquent();

819
var/SymfonyRequirements.php Normal file
View File

@ -0,0 +1,819 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/*
* Users of PHP 5.2 should be able to run the requirements checks.
* This is why the file and all classes must be compatible with PHP 5.2+
* (e.g. not using namespaces and closures).
*
* ************** CAUTION **************
*
* DO NOT EDIT THIS FILE as it will be overridden by Composer as part of
* the installation/update process. The original file resides in the
* SensioDistributionBundle.
*
* ************** CAUTION **************
*/
/**
* Represents a single PHP requirement, e.g. an installed extension.
* It can be a mandatory requirement or an optional recommendation.
* There is a special subclass, named PhpIniRequirement, to check a php.ini configuration.
*
* @author Tobias Schultze <http://tobion.de>
*/
class Requirement
{
private $fulfilled;
private $testMessage;
private $helpText;
private $helpHtml;
private $optional;
/**
* Constructor that initializes the requirement.
*
* @param bool $fulfilled Whether the requirement is fulfilled
* @param string $testMessage The message for testing the requirement
* @param string $helpHtml The help text formatted in HTML for resolving the problem
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
* @param bool $optional Whether this is only an optional recommendation not a mandatory requirement
*/
public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false)
{
$this->fulfilled = (bool) $fulfilled;
$this->testMessage = (string) $testMessage;
$this->helpHtml = (string) $helpHtml;
$this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText;
$this->optional = (bool) $optional;
}
/**
* Returns whether the requirement is fulfilled.
*
* @return bool true if fulfilled, otherwise false
*/
public function isFulfilled()
{
return $this->fulfilled;
}
/**
* Returns the message for testing the requirement.
*
* @return string The test message
*/
public function getTestMessage()
{
return $this->testMessage;
}
/**
* Returns the help text for resolving the problem.
*
* @return string The help text
*/
public function getHelpText()
{
return $this->helpText;
}
/**
* Returns the help text formatted in HTML.
*
* @return string The HTML help
*/
public function getHelpHtml()
{
return $this->helpHtml;
}
/**
* Returns whether this is only an optional recommendation and not a mandatory requirement.
*
* @return bool true if optional, false if mandatory
*/
public function isOptional()
{
return $this->optional;
}
}
/**
* Represents a PHP requirement in form of a php.ini configuration.
*
* @author Tobias Schultze <http://tobion.de>
*/
class PhpIniRequirement extends Requirement
{
/**
* Constructor that initializes the requirement.
*
* @param string $cfgName The configuration name used for ini_get()
* @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false,
* or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
* @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
* This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
* Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
* @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
* @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
* @param bool $optional Whether this is only an optional recommendation not a mandatory requirement
*/
public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false)
{
$cfgValue = ini_get($cfgName);
if (is_callable($evaluation)) {
if (null === $testMessage || null === $helpHtml) {
throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.');
}
$fulfilled = call_user_func($evaluation, $cfgValue);
} else {
if (null === $testMessage) {
$testMessage = sprintf('%s %s be %s in php.ini',
$cfgName,
$optional ? 'should' : 'must',
$evaluation ? 'enabled' : 'disabled'
);
}
if (null === $helpHtml) {
$helpHtml = sprintf('Set <strong>%s</strong> to <strong>%s</strong> in php.ini<a href="#phpini">*</a>.',
$cfgName,
$evaluation ? 'on' : 'off'
);
}
$fulfilled = $evaluation == $cfgValue;
}
parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional);
}
}
/**
* A RequirementCollection represents a set of Requirement instances.
*
* @author Tobias Schultze <http://tobion.de>
*/
class RequirementCollection implements IteratorAggregate
{
/**
* @var Requirement[]
*/
private $requirements = array();
/**
* Gets the current RequirementCollection as an Iterator.
*
* @return Traversable A Traversable interface
*/
public function getIterator()
{
return new ArrayIterator($this->requirements);
}
/**
* Adds a Requirement.
*
* @param Requirement $requirement A Requirement instance
*/
public function add(Requirement $requirement)
{
$this->requirements[] = $requirement;
}
/**
* Adds a mandatory requirement.
*
* @param bool $fulfilled Whether the requirement is fulfilled
* @param string $testMessage The message for testing the requirement
* @param string $helpHtml The help text formatted in HTML for resolving the problem
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
*/
public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null)
{
$this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false));
}
/**
* Adds an optional recommendation.
*
* @param bool $fulfilled Whether the recommendation is fulfilled
* @param string $testMessage The message for testing the recommendation
* @param string $helpHtml The help text formatted in HTML for resolving the problem
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
*/
public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null)
{
$this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true));
}
/**
* Adds a mandatory requirement in form of a php.ini configuration.
*
* @param string $cfgName The configuration name used for ini_get()
* @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false,
* or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
* @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
* This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
* Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
* @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
* @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
*/
public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null)
{
$this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false));
}
/**
* Adds an optional recommendation in form of a php.ini configuration.
*
* @param string $cfgName The configuration name used for ini_get()
* @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false,
* or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement
* @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false.
* This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin.
* Example: You require a config to be true but PHP later removes this config and defaults it to true internally.
* @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived)
* @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived)
* @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags)
*/
public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null)
{
$this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true));
}
/**
* Adds a requirement collection to the current set of requirements.
*
* @param RequirementCollection $collection A RequirementCollection instance
*/
public function addCollection(RequirementCollection $collection)
{
$this->requirements = array_merge($this->requirements, $collection->all());
}
/**
* Returns both requirements and recommendations.
*
* @return Requirement[]
*/
public function all()
{
return $this->requirements;
}
/**
* Returns all mandatory requirements.
*
* @return Requirement[]
*/
public function getRequirements()
{
$array = array();
foreach ($this->requirements as $req) {
if (!$req->isOptional()) {
$array[] = $req;
}
}
return $array;
}
/**
* Returns the mandatory requirements that were not met.
*
* @return Requirement[]
*/
public function getFailedRequirements()
{
$array = array();
foreach ($this->requirements as $req) {
if (!$req->isFulfilled() && !$req->isOptional()) {
$array[] = $req;
}
}
return $array;
}
/**
* Returns all optional recommendations.
*
* @return Requirement[]
*/
public function getRecommendations()
{
$array = array();
foreach ($this->requirements as $req) {
if ($req->isOptional()) {
$array[] = $req;
}
}
return $array;
}
/**
* Returns the recommendations that were not met.
*
* @return Requirement[]
*/
public function getFailedRecommendations()
{
$array = array();
foreach ($this->requirements as $req) {
if (!$req->isFulfilled() && $req->isOptional()) {
$array[] = $req;
}
}
return $array;
}
/**
* Returns whether a php.ini configuration is not correct.
*
* @return bool php.ini configuration problem?
*/
public function hasPhpIniConfigIssue()
{
foreach ($this->requirements as $req) {
if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) {
return true;
}
}
return false;
}
/**
* Returns the PHP configuration file (php.ini) path.
*
* @return string|false php.ini file path
*/
public function getPhpIniConfigPath()
{
return get_cfg_var('cfg_file_path');
}
}
/**
* This class specifies all requirements and optional recommendations that
* are necessary to run the Symfony Standard Edition.
*
* @author Tobias Schultze <http://tobion.de>
* @author Fabien Potencier <fabien@symfony.com>
*/
class SymfonyRequirements extends RequirementCollection
{
const LEGACY_REQUIRED_PHP_VERSION = '5.3.3';
const REQUIRED_PHP_VERSION = '5.5.9';
/**
* Constructor that initializes the requirements.
*/
public function __construct()
{
/* mandatory requirements follow */
$installedPhpVersion = phpversion();
$requiredPhpVersion = $this->getPhpRequiredVersion();
$this->addRecommendation(
$requiredPhpVersion,
'Vendors should be installed in order to check all requirements.',
'Run the <code>composer install</code> command.',
'Run the "composer install" command.'
);
if (false !== $requiredPhpVersion) {
$this->addRequirement(
version_compare($installedPhpVersion, $requiredPhpVersion, '>='),
sprintf('PHP version must be at least %s (%s installed)', $requiredPhpVersion, $installedPhpVersion),
sprintf('You are running PHP version "<strong>%s</strong>", but Symfony needs at least PHP "<strong>%s</strong>" to run.
Before using Symfony, upgrade your PHP installation, preferably to the latest version.',
$installedPhpVersion, $requiredPhpVersion),
sprintf('Install PHP %s or newer (installed version is %s)', $requiredPhpVersion, $installedPhpVersion)
);
}
$this->addRequirement(
version_compare($installedPhpVersion, '5.3.16', '!='),
'PHP version must not be 5.3.16 as Symfony won\'t work properly with it',
'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)'
);
$this->addRequirement(
is_dir(__DIR__.'/../vendor/composer'),
'Vendor libraries must be installed',
'Vendor libraries are missing. Install composer following instructions from <a href="http://getcomposer.org/">http://getcomposer.org/</a>. '.
'Then run "<strong>php composer.phar install</strong>" to install them.'
);
$cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache';
$this->addRequirement(
is_writable($cacheDir),
'app/cache/ or var/cache/ directory must be writable',
'Change the permissions of either "<strong>app/cache/</strong>" or "<strong>var/cache/</strong>" directory so that the web server can write into it.'
);
$logsDir = is_dir(__DIR__.'/../var/logs') ? __DIR__.'/../var/logs' : __DIR__.'/logs';
$this->addRequirement(
is_writable($logsDir),
'app/logs/ or var/logs/ directory must be writable',
'Change the permissions of either "<strong>app/logs/</strong>" or "<strong>var/logs/</strong>" directory so that the web server can write into it.'
);
if (version_compare($installedPhpVersion, '7.0.0', '<')) {
$this->addPhpIniRequirement(
'date.timezone', true, false,
'date.timezone setting must be set',
'Set the "<strong>date.timezone</strong>" setting in php.ini<a href="#phpini">*</a> (like Europe/Paris).'
);
}
if (false !== $requiredPhpVersion && version_compare($installedPhpVersion, $requiredPhpVersion, '>=')) {
$timezones = array();
foreach (DateTimeZone::listAbbreviations() as $abbreviations) {
foreach ($abbreviations as $abbreviation) {
$timezones[$abbreviation['timezone_id']] = true;
}
}
$this->addRequirement(
isset($timezones[@date_default_timezone_get()]),
sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()),
'Your default timezone is not supported by PHP. Check for typos in your <strong>php.ini</strong> file and have a look at the list of deprecated timezones at <a href="http://php.net/manual/en/timezones.others.php">http://php.net/manual/en/timezones.others.php</a>.'
);
}
$this->addRequirement(
function_exists('iconv'),
'iconv() must be available',
'Install and enable the <strong>iconv</strong> extension.'
);
$this->addRequirement(
function_exists('json_encode'),
'json_encode() must be available',
'Install and enable the <strong>JSON</strong> extension.'
);
$this->addRequirement(
function_exists('session_start'),
'session_start() must be available',
'Install and enable the <strong>session</strong> extension.'
);
$this->addRequirement(
function_exists('ctype_alpha'),
'ctype_alpha() must be available',
'Install and enable the <strong>ctype</strong> extension.'
);
$this->addRequirement(
function_exists('token_get_all'),
'token_get_all() must be available',
'Install and enable the <strong>Tokenizer</strong> extension.'
);
$this->addRequirement(
function_exists('simplexml_import_dom'),
'simplexml_import_dom() must be available',
'Install and enable the <strong>SimpleXML</strong> extension.'
);
if (function_exists('apc_store') && ini_get('apc.enabled')) {
if (version_compare($installedPhpVersion, '5.4.0', '>=')) {
$this->addRequirement(
version_compare(phpversion('apc'), '3.1.13', '>='),
'APC version must be at least 3.1.13 when using PHP 5.4',
'Upgrade your <strong>APC</strong> extension (3.1.13+).'
);
} else {
$this->addRequirement(
version_compare(phpversion('apc'), '3.0.17', '>='),
'APC version must be at least 3.0.17',
'Upgrade your <strong>APC</strong> extension (3.0.17+).'
);
}
}
$this->addPhpIniRequirement('detect_unicode', false);
if (extension_loaded('suhosin')) {
$this->addPhpIniRequirement(
'suhosin.executor.include.whitelist',
create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'),
false,
'suhosin.executor.include.whitelist must be configured correctly in php.ini',
'Add "<strong>phar</strong>" to <strong>suhosin.executor.include.whitelist</strong> in php.ini<a href="#phpini">*</a>.'
);
}
if (extension_loaded('xdebug')) {
$this->addPhpIniRequirement(
'xdebug.show_exception_trace', false, true
);
$this->addPhpIniRequirement(
'xdebug.scream', false, true
);
$this->addPhpIniRecommendation(
'xdebug.max_nesting_level',
create_function('$cfgValue', 'return $cfgValue > 100;'),
true,
'xdebug.max_nesting_level should be above 100 in php.ini',
'Set "<strong>xdebug.max_nesting_level</strong>" to e.g. "<strong>250</strong>" in php.ini<a href="#phpini">*</a> to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.'
);
}
$pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null;
$this->addRequirement(
null !== $pcreVersion,
'PCRE extension must be available',
'Install the <strong>PCRE</strong> extension (version 8.0+).'
);
if (extension_loaded('mbstring')) {
$this->addPhpIniRequirement(
'mbstring.func_overload',
create_function('$cfgValue', 'return (int) $cfgValue === 0;'),
true,
'string functions should not be overloaded',
'Set "<strong>mbstring.func_overload</strong>" to <strong>0</strong> in php.ini<a href="#phpini">*</a> to disable function overloading by the mbstring extension.'
);
}
/* optional recommendations follow */
if (file_exists(__DIR__.'/../vendor/composer')) {
require_once __DIR__.'/../vendor/autoload.php';
try {
$r = new ReflectionClass('Sensio\Bundle\DistributionBundle\SensioDistributionBundle');
$contents = file_get_contents(dirname($r->getFileName()).'/Resources/skeleton/app/SymfonyRequirements.php');
} catch (ReflectionException $e) {
$contents = '';
}
$this->addRecommendation(
file_get_contents(__FILE__) === $contents,
'Requirements file should be up-to-date',
'Your requirements file is outdated. Run composer install and re-check your configuration.'
);
}
$this->addRecommendation(
version_compare($installedPhpVersion, '5.3.4', '>='),
'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions',
'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.'
);
$this->addRecommendation(
version_compare($installedPhpVersion, '5.3.8', '>='),
'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156',
'Install PHP 5.3.8 or newer if your project uses annotations.'
);
$this->addRecommendation(
version_compare($installedPhpVersion, '5.4.0', '!='),
'You should not use PHP 5.4.0 due to the PHP bug #61453',
'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.'
);
$this->addRecommendation(
version_compare($installedPhpVersion, '5.4.11', '>='),
'When using the logout handler from the Symfony Security Component, you should have at least PHP 5.4.11 due to PHP bug #63379 (as a workaround, you can also set invalidate_session to false in the security logout handler configuration)',
'Install PHP 5.4.11 or newer if your project uses the logout handler from the Symfony Security Component.'
);
$this->addRecommendation(
(version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<'))
||
version_compare($installedPhpVersion, '5.4.8', '>='),
'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909',
'Install PHP 5.3.18+ or PHP 5.4.8+ if you want nice error messages for all fatal errors in the development environment.'
);
if (null !== $pcreVersion) {
$this->addRecommendation(
$pcreVersion >= 8.0,
sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion),
'<strong>PCRE 8.0+</strong> is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Symfony probably works anyway but it is recommended to upgrade your PCRE extension.'
);
}
$this->addRecommendation(
class_exists('DomDocument'),
'PHP-DOM and PHP-XML modules should be installed',
'Install and enable the <strong>PHP-DOM</strong> and the <strong>PHP-XML</strong> modules.'
);
$this->addRecommendation(
function_exists('mb_strlen'),
'mb_strlen() should be available',
'Install and enable the <strong>mbstring</strong> extension.'
);
$this->addRecommendation(
function_exists('iconv'),
'iconv() should be available',
'Install and enable the <strong>iconv</strong> extension.'
);
$this->addRecommendation(
function_exists('utf8_decode'),
'utf8_decode() should be available',
'Install and enable the <strong>XML</strong> extension.'
);
$this->addRecommendation(
function_exists('filter_var'),
'filter_var() should be available',
'Install and enable the <strong>filter</strong> extension.'
);
if (!defined('PHP_WINDOWS_VERSION_BUILD')) {
$this->addRecommendation(
function_exists('posix_isatty'),
'posix_isatty() should be available',
'Install and enable the <strong>php_posix</strong> extension (used to colorize the CLI output).'
);
}
$this->addRecommendation(
extension_loaded('intl'),
'intl extension should be available',
'Install and enable the <strong>intl</strong> extension (used for validators).'
);
if (extension_loaded('intl')) {
// in some WAMP server installations, new Collator() returns null
$this->addRecommendation(
null !== new Collator('fr_FR'),
'intl extension should be correctly configured',
'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.'
);
// check for compatible ICU versions (only done when you have the intl extension)
if (defined('INTL_ICU_VERSION')) {
$version = INTL_ICU_VERSION;
} else {
$reflector = new ReflectionExtension('intl');
ob_start();
$reflector->info();
$output = strip_tags(ob_get_clean());
preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches);
$version = $matches[1];
}
$this->addRecommendation(
version_compare($version, '4.0', '>='),
'intl ICU version should be at least 4+',
'Upgrade your <strong>intl</strong> extension with a newer ICU version (4+).'
);
if (class_exists('Symfony\Component\Intl\Intl')) {
$this->addRecommendation(
\Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion(),
sprintf('intl ICU version installed on your system is outdated (%s) and does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()),
'To get the latest internationalization data upgrade the ICU system package and the intl PHP extension.'
);
if (\Symfony\Component\Intl\Intl::getIcuDataVersion() <= \Symfony\Component\Intl\Intl::getIcuVersion()) {
$this->addRecommendation(
\Symfony\Component\Intl\Intl::getIcuDataVersion() === \Symfony\Component\Intl\Intl::getIcuVersion(),
sprintf('intl ICU version installed on your system (%s) does not match the ICU data bundled with Symfony (%s)', \Symfony\Component\Intl\Intl::getIcuVersion(), \Symfony\Component\Intl\Intl::getIcuDataVersion()),
'To avoid internationalization data inconsistencies upgrade the symfony/intl component.'
);
}
}
$this->addPhpIniRecommendation(
'intl.error_level',
create_function('$cfgValue', 'return (int) $cfgValue === 0;'),
true,
'intl.error_level should be 0 in php.ini',
'Set "<strong>intl.error_level</strong>" to "<strong>0</strong>" in php.ini<a href="#phpini">*</a> to inhibit the messages when an error occurs in ICU functions.'
);
}
$accelerator =
(extension_loaded('eaccelerator') && ini_get('eaccelerator.enable'))
||
(extension_loaded('apc') && ini_get('apc.enabled'))
||
(extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable'))
||
(extension_loaded('Zend OPcache') && ini_get('opcache.enable'))
||
(extension_loaded('xcache') && ini_get('xcache.cacher'))
||
(extension_loaded('wincache') && ini_get('wincache.ocenabled'))
;
$this->addRecommendation(
$accelerator,
'a PHP accelerator should be installed',
'Install and/or enable a <strong>PHP accelerator</strong> (highly recommended).'
);
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
$this->addRecommendation(
$this->getRealpathCacheSize() >= 5 * 1024 * 1024,
'realpath_cache_size should be at least 5M in php.ini',
'Setting "<strong>realpath_cache_size</strong>" to e.g. "<strong>5242880</strong>" or "<strong>5M</strong>" in php.ini<a href="#phpini">*</a> may improve performance on Windows significantly in some cases.'
);
}
$this->addPhpIniRecommendation('short_open_tag', false);
$this->addPhpIniRecommendation('magic_quotes_gpc', false, true);
$this->addPhpIniRecommendation('register_globals', false, true);
$this->addPhpIniRecommendation('session.auto_start', false);
$this->addRecommendation(
class_exists('PDO'),
'PDO should be installed',
'Install <strong>PDO</strong> (mandatory for Doctrine).'
);
if (class_exists('PDO')) {
$drivers = PDO::getAvailableDrivers();
$this->addRecommendation(
count($drivers) > 0,
sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'),
'Install <strong>PDO drivers</strong> (mandatory for Doctrine).'
);
}
}
/**
* Loads realpath_cache_size from php.ini and converts it to int.
*
* (e.g. 16k is converted to 16384 int)
*
* @return int
*/
protected function getRealpathCacheSize()
{
$size = ini_get('realpath_cache_size');
$size = trim($size);
$unit = strtolower(substr($size, -1, 1));
switch ($unit) {
case 'g':
return $size * 1024 * 1024 * 1024;
case 'm':
return $size * 1024 * 1024;
case 'k':
return $size * 1024;
default:
return (int) $size;
}
}
/**
* Defines PHP required version from Symfony version.
*
* @return string|false The PHP required version or false if it could not be guessed
*/
protected function getPhpRequiredVersion()
{
if (!file_exists($path = __DIR__.'/../composer.lock')) {
return false;
}
$composerLock = json_decode(file_get_contents($path), true);
foreach ($composerLock['packages'] as $package) {
$name = $package['name'];
if ('symfony/symfony' !== $name && 'symfony/http-kernel' !== $name) {
continue;
}
return (int) $package['version'][1] > 2 ? self::REQUIRED_PHP_VERSION : self::LEGACY_REQUIRED_PHP_VERSION;
}
return false;
}
}

0
var/cache/.gitkeep vendored Normal file
View File

0
var/logs/.gitkeep Normal file
View File

0
var/sessions/.gitkeep Normal file
View File

68
web/.htaccess Normal file
View File

@ -0,0 +1,68 @@
# Use the front controller as index file. It serves as a fallback solution when
# every other rewrite/redirect fails (e.g. in an aliased environment without
# mod_rewrite). Additionally, this reduces the matching process for the
# start page (path "/") because otherwise Apache will apply the rewriting rules
# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl).
DirectoryIndex app.php
# By default, Apache does not evaluate symbolic links if you did not enable this
# feature in your server configuration. Uncomment the following line if you
# install assets as symlinks or if you experience problems related to symlinks
# when compiling LESS/Sass/CoffeScript assets.
# Options FollowSymlinks
# Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve
# to the front controller "/app.php" but be rewritten to "/app.php/app".
<IfModule mod_negotiation.c>
Options -MultiViews
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
# Determine the RewriteBase automatically and set it as environment variable.
# If you are using Apache aliases to do mass virtual hosting or installed the
# project in a subdirectory, the base path will be prepended to allow proper
# resolution of the app.php file and to redirect to the correct URI. It will
# work in environments without path prefix as well, providing a safe, one-size
# fits all solution. But as you do not need it in this case, you can comment
# the following 2 lines to eliminate the overhead.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
# Sets the HTTP_AUTHORIZATION header removed by Apache
RewriteCond %{HTTP:Authorization} .
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect to URI without front controller to prevent duplicate content
# (with and without `/app.php`). Only do this redirect on the initial
# rewrite by Apache and not on subsequent cycles. Otherwise we would get an
# endless redirect loop (request -> rewrite to front controller ->
# redirect -> request -> ...).
# So in case you get a "too many redirects" error or you always get redirected
# to the start page because your Apache does not expose the REDIRECT_STATUS
# environment variable, you have 2 choices:
# - disable this feature by commenting the following 2 lines or
# - use Apache >= 2.3.9 and replace all L flags by END flags and remove the
# following RewriteCond (best solution)
RewriteCond %{ENV:REDIRECT_STATUS} ^$
RewriteRule ^app\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
# If the requested filename exists, simply serve it.
# We only want to let Apache serve files and not directories.
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]
# Rewrite all other queries to the front controller.
RewriteRule ^ %{ENV:BASE}/app.php [L]
</IfModule>
<IfModule !mod_rewrite.c>
<IfModule mod_alias.c>
# When mod_rewrite is not available, we instruct a temporary redirect of
# the start page to the front controller explicitly so that the website
# and the generated links can still be used.
RedirectMatch 302 ^/$ /app.php/
# RedirectTemp cannot be used instead
</IfModule>
</IfModule>

18
web/app.php Normal file
View File

@ -0,0 +1,18 @@
<?php
use Symfony\Component\HttpFoundation\Request;
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__.'/../app/autoload.php';
include_once __DIR__.'/../var/bootstrap.php.cache';
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
//$kernel = new AppCache($kernel);
// When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter
//Request::enableHttpMethodParameterOverride();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

40
web/app_dev.php Normal file
View File

@ -0,0 +1,40 @@
<?php
use Symfony\Component\HttpFoundation\Request;
// use Symfony\Component\Debug\Debug;
// If you don't want to setup permissions the proper way, just uncomment the following PHP line
// read http://symfony.com/doc/current/book/installation.html#checking-symfony-application-configuration-and-setup
// for more information
//umask(0000);
// This check prevents access to debug front controllers that are deployed by accident to production servers.
// Feel free to remove this, extend it, or make something more sophisticated.
if (isset($_SERVER['HTTP_CLIENT_IP'])
|| isset($_SERVER['HTTP_X_FORWARDED_FOR'])
|| !(in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1']) || php_sapi_name() === 'cli-server')
) {
header('HTTP/1.0 403 Forbidden');
exit('You are not allowed to access this file. Check '.basename(__FILE__).' for more information.');
}
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require __DIR__.'/../app/autoload.php';
// Disable error handling page:
// Debug::enable();
set_exception_handler(function($e) {
$msg = array();
$msg[] = 'Exception: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine();
$msg[] = '';
$msg[] = $e->getTraceAsString();
echo implode("\n", $msg);
});
$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);

BIN
web/apple-touch-icon.png Normal file

Binary file not shown.

After

(image error) Size: 2.0 KiB

422
web/config.php Normal file
View File

@ -0,0 +1,422 @@
<?php
/*
* ************** CAUTION **************
*
* DO NOT EDIT THIS FILE as it will be overridden by Composer as part of
* the installation/update process. The original file resides in the
* SensioDistributionBundle.
*
* ************** CAUTION **************
*/
if (!isset($_SERVER['HTTP_HOST'])) {
exit('This script cannot be run from the CLI. Run it from a browser.');
}
if (!in_array(@$_SERVER['REMOTE_ADDR'], array(
'127.0.0.1',
'::1',
))) {
header('HTTP/1.0 403 Forbidden');
exit('This script is only accessible from localhost.');
}
require_once dirname(__FILE__).'/../var/SymfonyRequirements.php';
$symfonyRequirements = new SymfonyRequirements();
$majorProblems = $symfonyRequirements->getFailedRequirements();
$minorProblems = $symfonyRequirements->getFailedRecommendations();
$hasMajorProblems = (bool) count($majorProblems);
$hasMinorProblems = (bool) count($minorProblems);
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="robots" content="noindex,nofollow" />
<title>Symfony Configuration Checker</title>
<style>
/* styles copied from symfony framework bundle */
html {
background: #eee;
}
body {
font: 11px Verdana, Arial, sans-serif;
color: #333;
}
.sf-reset, .sf-reset .block, .sf-reset #message {
margin: auto;
}
img {
border: 0;
}
.clear {
clear: both;
height: 0;
font-size: 0;
line-height: 0;
}
.clear-fix:after {
content: "\0020";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.clear-fix {
display: inline-block;
}
* html .clear-fix {
height: 1%;
}
.clear-fix {
display: block;
}
.header {
padding: 30px 30px 20px 30px;
}
.header-logo {
float: left;
}
.search {
float: right;
padding-top: 20px;
}
.search label {
line-height: 28px;
vertical-align: middle;
}
.search input {
width: 195px;
font-size: 12px;
border: 1px solid #dadada;
background: #fff url(data:image/gif;base64,R0lGODlhAQAFAKIAAPX19e/v7/39/fr6+urq6gAAAAAAAAAAACH5BAAAAAAALAAAAAABAAUAAAMESAEjCQA7) repeat-x left top;
padding: 5px 6px;
color: #565656;
}
.search input[type="search"] {
-webkit-appearance: textfield;
}
#content {
width: 970px;
margin: 0 auto;
}
#content pre {
white-space: normal;
font-family: Arial, Helvetica, sans-serif;
}
/*
Copyright (c) 2010, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.com/yui/license.html
version: 3.1.2
build: 56
*/
.sf-reset div,.sf-reset dl,.sf-reset dt,.sf-reset dd,.sf-reset ul,.sf-reset ol,.sf-reset li,.sf-reset h1,.sf-reset h2,.sf-reset h3,.sf-reset h4,.sf-reset h5,.sf-reset h6,.sf-reset pre,.sf-reset code,.sf-reset form,.sf-reset fieldset,.sf-reset legend,.sf-reset input,.sf-reset textarea,.sf-reset p,.sf-reset blockquote,.sf-reset th,.sf-reset td{margin:0;padding:0;}.sf-reset table{border-collapse:collapse;border-spacing:0;}.sf-reset fieldset,.sf-reset img{border:0;}.sf-reset address,.sf-reset caption,.sf-reset cite,.sf-reset code,.sf-reset dfn,.sf-reset em,.sf-reset strong,.sf-reset th,.sf-reset var{font-style:normal;font-weight:normal;}.sf-reset li{list-style:none;}.sf-reset caption,.sf-reset th{text-align:left;}.sf-reset h1,.sf-reset h2,.sf-reset h3,.sf-reset h4,.sf-reset h5,.sf-reset h6{font-size:100%;font-weight:normal;}.sf-reset q:before,.sf-reset q:after{content:'';}.sf-reset abbr,.sf-reset acronym{border:0;font-variant:normal;}.sf-reset sup{vertical-align:text-top;}.sf-reset sub{vertical-align:text-bottom;}.sf-reset input,.sf-reset textarea,.sf-reset select{font-family:inherit;font-size:inherit;font-weight:inherit;}.sf-reset input,.sf-reset textarea,.sf-reset select{font-size:100%;}.sf-reset legend{color:#000;}
.sf-reset abbr {
border-bottom: 1px dotted #000;
cursor: help;
}
.sf-reset p {
font-size: 14px;
line-height: 20px;
padding-bottom: 20px;
}
.sf-reset strong {
color: #313131;
font-weight: bold;
}
.sf-reset a {
color: #6c6159;
}
.sf-reset a img {
border: none;
}
.sf-reset a:hover {
text-decoration: underline;
}
.sf-reset em {
font-style: italic;
}
.sf-reset h2,
.sf-reset h3 {
font-weight: bold;
}
.sf-reset h1 {
font-family: Georgia, "Times New Roman", Times, serif;
font-size: 20px;
color: #313131;
word-wrap: break-word;
}
.sf-reset li {
padding-bottom: 10px;
}
.sf-reset .block {
-moz-border-radius: 16px;
-webkit-border-radius: 16px;
border-radius: 16px;
margin-bottom: 20px;
background-color: #FFFFFF;
border: 1px solid #dfdfdf;
padding: 40px 50px;
word-break: break-all;
}
.sf-reset h2 {
font-size: 16px;
font-family: Arial, Helvetica, sans-serif;
}
.sf-reset li a {
background: none;
color: #868686;
text-decoration: none;
}
.sf-reset li a:hover {
background: none;
color: #313131;
text-decoration: underline;
}
.sf-reset ol {
padding: 10px 0;
}
.sf-reset ol li {
list-style: decimal;
margin-left: 20px;
padding: 2px;
padding-bottom: 20px;
}
.sf-reset ol ol li {
list-style-position: inside;
margin-left: 0;
white-space: nowrap;
font-size: 12px;
padding-bottom: 0;
}
.sf-reset li .selected {
background-color: #ffd;
}
.sf-button {
display: -moz-inline-box;
display: inline-block;
text-align: center;
vertical-align: middle;
border: 0;
background: transparent none;
text-transform: uppercase;
cursor: pointer;
font: bold 11px Arial, Helvetica, sans-serif;
}
.sf-button span {
text-decoration: none;
display: block;
height: 28px;
float: left;
}
.sf-button .border-l {
text-decoration: none;
display: block;
height: 28px;
float: left;
padding: 0 0 0 7px;
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAcCAYAAACtQ6WLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQtJREFUeNpiPHnyJAMakARiByDWYEGT8ADiYGVlZStubm5xlv///4MEQYoKZGRkQkRERLRYWVl5wYJQyXBZWdkwCQkJUxAHKgaWlAHSLqKiosb//v1DsYMFKGCvoqJiDmQzwXTAJYECulxcXNLoumCSoszMzDzoumDGghQwYZUECWIzkrAkSIIGOmlkLI10AiX//P379x8jIyMTNmPf/v79+ysLCwsvuiQoNi5//fr1Kch4dAyS3P/gwYMTQBP+wxwHw0xA4gkQ73v9+vUZdJ2w1Lf82bNn4iCHCQoKasHsZw4ODgbRIL8c+/Lly5M3b978Y2dn5wC6npkFLXnsAOKLjx49AmUHLYAAAwBoQubG016R5wAAAABJRU5ErkJggg==) no-repeat top left;
}
.sf-button .border-r {
padding: 0 7px 0 0;
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAcCAYAAACtQ6WLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAR1JREFUeNpiPHnyZCMDA8MNID5gZmb2nAEJMH7//v3N169fX969e/cYkL8WqGAHXPLv37//QYzfv39/fvPmzbUnT56sAXInmJub/2H5/x8sx8DCwsIrISFhDmQyPX78+CmQXs70798/BmQsKipqBNTgdvz4cWkmkE5kDATMioqKZkCFdiwg1eiAi4tLGqhQF24nMmBmZuYEigth1QkEbEBxTlySYPvJkwSJ00AnjYylgU6gxB8g/oFVEphkvgLF32KNMmCCewYUv4qhEyj47+HDhyeBzIMYOoEp8CxQw56wsLAncJ1//vz5/P79+2svX74EJc2V4BT58+fPd8CE/QKYHMGJOiIiAp6oWW7evDkNSF8DZYfIyEiU7AAQYACJ2vxVdJW4eQAAAABJRU5ErkJggg==) right top no-repeat;
}
.sf-button .btn-bg {
padding: 0 14px;
color: #636363;
line-height: 28px;
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAYAAACgXdXMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAClJREFUeNpiPnny5EKGf//+/Wf6//8/A4QAcrGzKCZwGc9sa2urBBBgAIbDUoYVp9lmAAAAAElFTkSuQmCC) repeat-x top left;
}
.sf-button:hover .border-l,
.sf-button-selected .border-l {
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAcCAYAAACtQ6WLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAR9JREFUeNpi/P//PwMyOHfunDqQSgNiexZkibNnzxYBqZa3HOs5v7PcYQBLnjlzhg1IbfzIdsTjA/t+ht9Mr8GKwZL//v3r+sB+0OMN+zqIEf8gFMvJkyd1gXTOa9YNDP//otrPAtSV/Jp9HfPff78Z0AEL0LUeXxivMfxD0wXTqfjj/2ugkf+wSrL9/YtpJEyS4S8WI5Ek/+GR/POPFjr//cenE6/kP9q4Fo/kr39/mdj+M/zFkGQCSj5i+ccPjLJ/GBgkuYOHQR1sNDpmAkb2LBmWwL///zKCIxwZM0VHR18G6p4uxeLLAA4tJMwEshiou1iMxXaHLGswA+t/YbhORuQUv2DBAnCifvxzI+enP3dQJUFg/vz5sOzgBBBgAPxX9j0YnH4JAAAAAElFTkSuQmCC) no-repeat top left;
}
.sf-button:hover .border-r,
.sf-button-selected .border-r {
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAcCAYAAACtQ6WLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAT5JREFUeNpiPHv27BkGBoaDQDzLyMjoJgMSYHrM3WX8hn1d0f///88DFRYhSzIuv2X5H8Rg/SfKIPDTkYH/l80OINffxMTkF9O/f/8ZQPgnwyuGl+wrGd6x7vf49+9fO9jYf3+Bkkj4NesmBqAV+SdPntQC6vzHgIz//gOawbqOGchOxtAJwp8Zr4F0e7D8/fuPAR38/P8eZIo0yz8skv8YvoIk+YE6/zNgAyD7sRqLkPzzjxY6/+HS+R+fTkZ8djLh08lCUCcuSWawJGbwMTGwg7zyBatX2Bj5QZKPsBrLzaICktzN8g/NWEYGZgYZjoC/wMiei5FMpFh8QPSU6Ojoy3Cd7EwiDBJsDgxiLNY7gLrKQGIsHAxSDHxAO2TZ/b8D+TVxcXF9MCtYtLiKLgDpfUDVsxITE1GyA0CAAQA2E/N8VuHyAAAAAABJRU5ErkJggg==) right top no-repeat;
}
.sf-button:hover .btn-bg,
.sf-button-selected .btn-bg {
color: #FFFFFF;
text-shadow:0 1px 1px #6b9311;
background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAcCAIAAAAvP0KbAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAEFJREFUeNpiPnv2LNMdvlymf///M/37B8R/QfQ/MP33L4j+B6Qh7L9//sHpf2h8MA1V+w/KRjYLaDaLCU8vQIABAFO3TxZriO4yAAAAAElFTkSuQmCC) repeat-x top left;
}
/* styles copied from bundles/sensiodistribution/webconfigurator/css/install.css */
body {
font-size: 14px;
font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
.sf-reset h1.title {
font-size: 45px;
padding-bottom: 30px;
}
.sf-reset h2 {
font-weight: bold;
color: #FFFFFF;
/* Font is reset to sans-serif (like body) */
font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
margin-bottom: 10px;
background-color: #aacd4e;
padding: 2px 4px;
display: inline-block;
text-transform: uppercase;
}
.sf-reset ul a,
.sf-reset ul a:hover {
background: url(../images/blue-arrow.png) no-repeat right 6px;
padding-right: 10px;
}
.sf-reset ul, ol {
padding-left: 20px;
}
.sf-reset li {
padding-bottom: 18px;
}
.sf-reset ol li {
list-style-type: decimal;
}
.sf-reset ul li {
list-style-type: none;
}
.sf-reset .symfony-blocks-install {
overflow: hidden;
}
.sf-reset .symfony-install-continue {
font-size: 0.95em;
padding-left: 0;
}
.sf-reset .symfony-install-continue li {
padding-bottom: 10px;
}
.sf-reset .ok {
color: #fff;
font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
background-color: #6d6;
padding: 10px;
margin-bottom: 20px;
}
.sf-reset .ko {
background-color: #d66;
}
.sf-reset p.help {
padding: 12px 16px;
word-break: break-word;
}
.version {
text-align: right;
font-size: 10px;
margin-right: 20px;
}
.sf-reset a,
.sf-reset li a {
color: #08C;
text-decoration: none;
}
.sf-reset a:hover,
.sf-reset li a:hover {
color: #08C;
text-decoration: underline;
}
.sf-reset textarea {
padding: 7px;
}
</style>
</head>
<body>
<div id="content">
<div class="header clear-fix">
<div class="header-logo">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALYAAAA+CAMAAACxzRGDAAAAUVBMVEX////Ly8yko6WLioxkYmVXVVkwLjLl5eWxsLJKSEzy8vJxcHLY2Ni+vb89Oz9XVVh+fH+Yl5n///+xsbLY2Nlxb3KkpKWXlph+fX+LiYy+vr/IZP61AAAAAXRSTlMAQObYZgAABRBJREFUeNrVmtuWoyAQRS1FEEQSzQU7//+hYxUiXsKQZLJWM+chsUloN+WhCuguYoKyYqzmvGasKqH4HyRKxndipcgcumH8qViTM7TkUclcwaHmf5XM0eWq4km1KjdqXfMXJHVe1J3hL8lk5fCGv6wmT+o0d87U+XNrk0Y9nfv+7LM6ZJH5ZBL6LAbSxQ3Q5FDr22Skr8PQSy4n7isnsQxSX4r6pobhjCHHeDNOKrO3yGmCvZOjV9jmt8ulTdXFKdbKLNh+kOMvBzuVRa4Y7MUsdEUSWQe7xxCfZmcwjHU83LqzFvSbJQOXQvptbPnEFoyZtUUGwTeKuLuTHyT1kaP0P6cR01OKvv448gtl61dqZfmJezQmU/t+1R2fJLtBwXV6uWGwB9SZPrn0fKO2WAvQN1PUhHjTom3xgXYTkvlSKHs19OhslETq6X3HrXbjt8XbGj9b4Gi+lUAnL6XxQj8Pyk9N4Bt1xUrsLVN/3isYMug8rODMdbgOvoHs8uAb2fcANIAzkKCLYy+AXRpSU8sr1r4P67xhLgPp7vM32zlqt7Bhq2fI1Hwp+VgANxok59SsGV3oqdUL0YVDMRY7Yg8QLbVUU4NZNoOq5hJHuxEM28Sh/IyUZ8D3reR+yc58EGvOy2U0HQL6G9V+kWyEWHmzaMx6t4o9RhOm/riUiYrzqij4Ptqkn7AaCXqc+F47m04ahfde7YIz8RHEBN6BdVwdIGRVdNbKqYu1Hc0x0wBY4wqC8+XUgBGnj81SZsQB+0yAS1x/BlI/6ebHHk0lauQLuPDpu6EwAVJ7T0rl2uXa23jcqNyOZekhqYHRz3JOANrF4wCCmEs1f9D1lUe0n4NAATed80Y5e0Q7CO2TezM/BR6wKdgQzKbCF4uOQC3Bk0fKAzbFlyRWg3gksA/gmm7eOjrpaKX7fHlEW2xLbE6GZsPiCiShVzN7RG2xTz2G+OJtEqzdJ7APxy3MrSsV0VukXbKMp9lhs5BN6dr3CN+sySUaoxGwfRUM3I/gdPYONgVU+PLX4vUWm32AvUySarbONvcpV2RQEPKKjEBHFk01kQDGRblnn8ZuE9g+JUl8OWAPbkFK2K6JxhJVvF47FzYYnAN22ttwxKYCoH36rheEB7KG/HF/YUaa2G5JF+55tpyrl7B1WHM39HuP2N2EXPl1UBu8vbj4OjvD+NoTE4ssF+ScARgaJY1N7+u8bY/Y9BSM5PKwJbvMVab32YP5FB5TtcYVrGoASolVLTzI7kVsYVxRtAb5n2JXq1vCdtd47XtYItynrN0835PasLg0y13aOPbmPI+on2Lr9e5tjSHvgkAvclUjL3Fsdaw03IzgTR62yYClk7QMah4IQ0qSsoYYbOix6zJR1ZGDNMOY3Bb6W5S6jiyovep3t7bUPyoq7OkjYumrfESp8zSBc/OLosVf+nTnnKjsqR16++WDwpI8FxJWRFTlI6NKnqYJaL96TqjAbo9Toi5QiWBDcmfdFV+T8dkvFe5bItgstbM2X6QG2mVun+cazfRwOS0eiaeRRJKgLfc3BQAqfnhJyz8lfR6580SF/FXVu83Nz1xrrnFqqXL6Qxl47DNSm4RFflvN5sABDD8peouqLLKQXVdGbnqf+qIpOxON4ZyYdJEJ6sy4zS2c5eRPTT4Jyp46qDE5/ptAWqJOQ9e6yE82FXBbZCk1/tXVoshVoopE3CB0zmraI3nbqCJ/gW3ZMgtbC5nh/QHlOoOZBxQCRgAAAABJRU5ErkJggg==" alt="Symfony" />
</div>
<div class="search">
<form method="get" action="http://symfony.com/search">
<div class="form-row">
<label for="search-id">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAABUElEQVQoz2NgAIJ29iBdD0d7X2cPb+tY2f9MDMjgP2O2hKu7vS8CBlisZUNSMJ3fxRMkXO61wm2ue6I3iB1q8Z8ZriDZFCS03fm/wX+1/xp/TBo8QPxeqf+MUAW+QIFKj/+q/wX/c/3n/i/6Qd/bx943z/Q/K1SBI1D9fKv/AhCn/Wf5L5EHdFGKw39OqAIXoPpOMziX4T9/DFBBnuN/HqhAEtCKCNf/XDA/rZRyAmrpsvrPDVUw3wrkqCiLaewg6TohX1d7X0ffs5r/OaAKfinmgt3t4ulr4+Xg4ANip3j+l/zPArNT4LNOD0pAgWCSOUIBy3+h/+pXbBa5tni0eMx23+/mB1YSYnENroT5Pw/QSOX/mkCo+l/jgo0v2KJA643s8PgAmsMBDCbu/5xALHPB2husxN9uCzsDOgAq5kAoaZVnYMCh5Ky1r88Eh/+iABM8jUk7ClYIAAAAAElFTkSuQmCC" alt="Search on Symfony website" />
</label>
<input name="q" id="search-id" type="search" placeholder="Search on Symfony website" />
<button type="submit" class="sf-button">
<span class="border-l">
<span class="border-r">
<span class="btn-bg">OK</span>
</span>
</span>
</button>
</div>
</form>
</div>
</div>
<div class="sf-reset">
<div class="block">
<div class="symfony-block-content">
<h1 class="title">Configuration Checker</h1>
<p>
This script analyzes your system to check whether is
ready to run Symfony applications.
</p>
<?php if ($hasMajorProblems): ?>
<h2 class="ko">Major problems</h2>
<p>Major problems have been detected and <strong>must</strong> be fixed before continuing:</p>
<ol>
<?php foreach ($majorProblems as $problem): ?>
<li><?php echo $problem->getTestMessage() ?>
<p class="help"><em><?php echo $problem->getHelpHtml() ?></em></p>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<?php if ($hasMinorProblems): ?>
<h2>Recommendations</h2>
<p>
<?php if ($hasMajorProblems): ?>Additionally, to<?php else: ?>To<?php endif; ?> enhance your Symfony experience,
it’s recommended that you fix the following:
</p>
<ol>
<?php foreach ($minorProblems as $problem): ?>
<li><?php echo $problem->getTestMessage() ?>
<p class="help"><em><?php echo $problem->getHelpHtml() ?></em></p>
</li>
<?php endforeach; ?>
</ol>
<?php endif; ?>
<?php if ($symfonyRequirements->hasPhpIniConfigIssue()): ?>
<p id="phpini">*
<?php if ($symfonyRequirements->getPhpIniConfigPath()): ?>
Changes to the <strong>php.ini</strong> file must be done in "<strong><?php echo $symfonyRequirements->getPhpIniConfigPath() ?></strong>".
<?php else: ?>
To change settings, create a "<strong>php.ini</strong>".
<?php endif; ?>
</p>
<?php endif; ?>
<?php if (!$hasMajorProblems && !$hasMinorProblems): ?>
<p class="ok">All checks passed successfully. Your system is ready to run Symfony applications.</p>
<?php endif; ?>
<ul class="symfony-install-continue">
<?php if ($hasMajorProblems || $hasMinorProblems): ?>
<li><a href="config.php">Re-check configuration</a></li>
<?php endif; ?>
</ul>
</div>
</div>
</div>
<div class="version">Symfony Standard Edition</div>
</div>
</body>
</html>

BIN
web/favicon.ico Normal file

Binary file not shown.

After

Width: 32px  |  Height: 32px  |  Size: 6.4 KiB

5
web/robots.txt Normal file
View File

@ -0,0 +1,5 @@
# www.robotstxt.org/
# www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449
User-agent: *
Disallow: