diff --git a/DOCUMENTS/LICENSE b/DOCUMENTS/LICENSE new file mode 100644 index 0000000..4dbb95c --- /dev/null +++ b/DOCUMENTS/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 M.Karminski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/DOCUMENTS/related_modules.md b/DOCUMENTS/related_modules.md new file mode 100644 index 0000000..ff2d6df --- /dev/null +++ b/DOCUMENTS/related_modules.md @@ -0,0 +1,19 @@ +related_modules.md +------------------- +``` +@version 151109:1 +@author karminski +``` + +PHP Module +------------ + +- curl +- json +- PDO +- pdo_mysql +- phpredis + - https://github.com/phpredis/phpredis +- mongodb +- rdkafka +- yac \ No newline at end of file diff --git a/README.md b/README.md index 510464c..87f223b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,26 @@ -# gq-hello-container +Saltfish Raw PHP +---------------- +``` +@version 180426:1 +@author karminski +``` + +Name +---- + + saltfish_raw_php + + +Table of Contents +----------------- + +* [Name](#name) +* [Desc](#desc) + +sdgdfghdf +Desc +---- + + saltfish API generator base framework PHP version. + diff --git a/config/cron/SampleBuilder.cron b/config/cron/SampleBuilder.cron new file mode 100644 index 0000000..a058661 --- /dev/null +++ b/config/cron/SampleBuilder.cron @@ -0,0 +1,7 @@ +# SampleBuilder.cron +# @version: 160309:1 +# @author: karminski +# + +# SampleBuilder cronjob +0 0 */1 * * root flock -xn /data/repo/saltfish_raw_php/lock/SampleBuilder.lock -c '/data/apps/php7/bin/php /data/repo/saltfish_raw_php/src/SampleBuilder.php --signal_file=/data/repo/saltfish_raw_php/lock/SampleBuilder.lock & >> /data/repo/saltfish_raw_php/logs/SampleBuilder.stdout.log' \ No newline at end of file diff --git a/config/jenkins/Jenkinsfile.beta b/config/jenkins/Jenkinsfile.beta new file mode 100644 index 0000000..0b455e9 --- /dev/null +++ b/config/jenkins/Jenkinsfile.beta @@ -0,0 +1,71 @@ +// Jenkinsfile.beta +// @version 180820:1 +// @author zhangxuhong +// + +def appName = "suid-generator" +def label = "worker-${UUID.randomUUID().toString()}" +def registryHost = "harbor02.juejin.id/beta/" +def deployLocation = "beta.kube01.lobj.juejin.id" + +podTemplate( + label: label, + containers: [ + containerTemplate( + name: 'git', + image: 'harbor02.juejin.id/infrastructure/git-1.8.3.1-centos:0.0.3', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'dind', + image: 'harbor02.juejin.id/infrastructure/docker-18.06.0-ce-dind:0.0.2', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'kubectl', + image: 'harbor02.juejin.id/infrastructure/kubectl-1.11.1-centos-with-cert-cluster01.lobj:latest', + command: 'cat', + ttyEnabled: true + ), + ], + volumes: [ + hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), + ], + cloud: deployLocation +){ + node(label) { + // start deploy phrase + stage("[1/4] check out"){ + container('git'){ + checkout scm + sh "git rev-parse --short HEAD > commit-id" + tag = readFile('commit-id').replace("\n", "").replace("\r", "") + imageName = "${registryHost}${appName}:${tag}" + env.BUILDIMG=imageName + } + } + stage("[2/4] Build"){ + container('dind'){ + sh "docker version" + sh "docker build -t ${imageName} ./" + } + } + stage("[3/4] Push"){ + container('dind'){ + sh "/usr/local/bin/docker-login.sh" + sh "docker push ${imageName}" + } + } + stage("[4/4] Deploy"){ + container('kubectl'){ + sh "sed -i 's#__IMAGE__#$BUILDIMG#g' ./config/kubernetes/Deployment.yaml" + sh "cat ./config/kubernetes/Deployment.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Service.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Ingress.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Deployment.yaml" + } + } + } +} diff --git a/config/jenkins/Jenkinsfile.prod b/config/jenkins/Jenkinsfile.prod new file mode 100644 index 0000000..5c5eb0c --- /dev/null +++ b/config/jenkins/Jenkinsfile.prod @@ -0,0 +1,71 @@ +// Jenkinsfile.prod +// @version 180820:1 +// @author zhangxuhong +// + +def appName = "suid-generator" +def label = "worker-${UUID.randomUUID().toString()}" +def registryHost = "harbor02.juejin.id/prod/" +def deployLocation = "prod.kube01.lobj.juejin.id" + +podTemplate( + label: label, + containers: [ + containerTemplate( + name: 'git', + image: 'harbor02.juejin.id/infrastructure/git-1.8.3.1-centos:0.0.3', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'dind', + image: 'harbor02.juejin.id/infrastructure/docker-18.06.0-ce-dind:0.0.2', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'kubectl', + image: 'harbor02.juejin.id/infrastructure/kubectl-1.11.1-centos-with-cert-cluster01.lobj:latest', + command: 'cat', + ttyEnabled: true + ), + ], + volumes: [ + hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), + ], + cloud: deployLocation +){ + node(label) { + // start deploy phrase + stage("[1/4] check out"){ + container('git'){ + checkout scm + sh "git rev-parse --short HEAD > commit-id" + tag = readFile('commit-id').replace("\n", "").replace("\r", "") + imageName = "${registryHost}${appName}:${tag}" + env.BUILDIMG=imageName + } + } + stage("[2/4] Build"){ + container('dind'){ + sh "docker version" + sh "docker build -t ${imageName} ./" + } + } + stage("[3/4] Push"){ + container('dind'){ + sh "/usr/local/bin/docker-login.sh" + sh "docker push ${imageName}" + } + } + stage("[4/4] Deploy"){ + container('kubectl'){ + sh "sed -i 's#__IMAGE__#$BUILDIMG#g' ./config/kubernetes/Deployment.yaml" + sh "cat ./config/kubernetes/Deployment.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Service.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Ingress.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/Deployment.yaml" + } + } + } +} diff --git a/config/jenkins/Jenkinsfile.test b/config/jenkins/Jenkinsfile.test new file mode 100644 index 0000000..6592bf6 --- /dev/null +++ b/config/jenkins/Jenkinsfile.test @@ -0,0 +1,82 @@ +// Jenkinsfile.test +// @version 180903:2 +// @author zhangxuhong +// + +def repoName = "gq-hello" +def nginxRepoName = "${repoName}-nginx" +def label = "worker-${UUID.randomUUID().toString()}" +def registryHost = "harbor02.juejin.id/test/" +def deployLocation = "test.kube01.lobj.juejin.id" + +podTemplate( + label: label, + containers: [ + containerTemplate( + name: 'git', + image: 'harbor02.juejin.id/infrastructure/git-1.8.3.1-centos:0.0.3', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'dind', + image: 'harbor02.juejin.id/infrastructure/docker-18.06.0-ce-dind:0.0.2', + command: 'cat', + ttyEnabled: true + ), + containerTemplate( + name: 'kubectl', + image: 'harbor02.juejin.id/infrastructure/kubectl-1.11.1-centos-with-cert-cluster01.lobj:latest', + command: 'cat', + ttyEnabled: true + ), + ], + volumes: [ + hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'), + ], + cloud: deployLocation +){ + node(label) { + // start deploy phrase + stage("[1/4] check out"){ + container('git'){ + checkout scm + sh "git rev-parse --short HEAD > commit-id" + tag = readFile('commit-id').replace("\n", "").replace("\r", "") + imageName = "${registryHost}${repoName}:${tag}" + nginxImageName = "${registryHost}${nginxRepoName}:${tag}" + env.IMAGE_NAME = imageName + env.NGINX_IMAGE_NAME = nginxImageName + } + } + stage("[2/4] Build"){ + container('dind'){ + sh "docker version" + sh "docker build -f gq-hello.dockerfile -t ${imageName} ./" + sh "docker build -f gq-hello-nginx.dockerfile -t ${nginxImageName} ./" + } + } + stage("[3/4] Push"){ + container('dind'){ + sh "/usr/local/bin/docker-login.sh" + sh "docker push ${imageName}" + sh "docker push ${nginxImageName}" + } + } + stage("[4/4] Deploy"){ + container('kubectl'){ + // deploy nginx + sh "sed -i 's#__IMAGE__#$NGINX_IMAGE_NAME#g' ./config/kubernetes/gq-hello-nginx-deployment.yaml" + sh "cat ./config/kubernetes/gq-hello-nginx-deployment.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/gq-hello-nginx-service.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/gq-hello-nginx-ingress.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/gq-hello-nginx-deployment.yaml" + // deploy repo + sh "sed -i 's#__IMAGE__#$IMAGE_NAME#g' ./config/kubernetes/gq-hello-deployment.yaml" + sh "cat ./config/kubernetes/gq-hello-deployment.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/gq-hello-service.yaml" + sh "kubectl --kubeconfig=/root/.kube/config apply -f ./config/kubernetes/gq-hello-deployment.yaml" + } + } + } +} diff --git a/config/kubernetes/gq-hello-deployment.yaml b/config/kubernetes/gq-hello-deployment.yaml new file mode 100644 index 0000000..a7a2554 --- /dev/null +++ b/config/kubernetes/gq-hello-deployment.yaml @@ -0,0 +1,39 @@ +# gq-hello-deployment.yaml +# +# @version 180806:2 +# @author zhangxuhong + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: gq-hello + labels: + name: gq-hello + role: backend + pl: php + application: php + version: 7.2.9 + division: infrastructure +spec: + replicas: 3 + selector: + matchLabels: + name: gq-hello + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + template: + metadata: + labels: + name: gq-hello + spec: + containers: + - name: gq-hello + image: __IMAGE__ + imagePullPolicy: Always + ports: + - name: gq-hello + containerPort: 9000 + protocol: TCP diff --git a/config/kubernetes/gq-hello-nginx-deployment.yaml b/config/kubernetes/gq-hello-nginx-deployment.yaml new file mode 100644 index 0000000..5683502 --- /dev/null +++ b/config/kubernetes/gq-hello-nginx-deployment.yaml @@ -0,0 +1,39 @@ +# gq-hello-nginx-deployment.yaml +# +# @version 180806:2 +# @author zhangxuhong + +kind: Deployment +apiVersion: apps/v1 +metadata: + name: gq-hello-nginx + labels: + name: gq-hello-nginx + role: backend + pl: c + application: nginx + version: 1.14.0 + division: infrastructure +spec: + replicas: 3 + selector: + matchLabels: + name: gq-hello-nginx + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 25% + maxSurge: 25% + template: + metadata: + labels: + name: gq-hello-nginx + spec: + containers: + - name: gq-hello-nginx + image: __IMAGE__ + imagePullPolicy: Always + ports: + - name: gq-hello-nginx + containerPort: 80 + protocol: TCP diff --git a/config/kubernetes/gq-hello-nginx-ingress.yaml b/config/kubernetes/gq-hello-nginx-ingress.yaml new file mode 100644 index 0000000..9f5ce82 --- /dev/null +++ b/config/kubernetes/gq-hello-nginx-ingress.yaml @@ -0,0 +1,26 @@ +# gq-hello-nginx-ingress.yaml +# +# @version: 180627:1 +# @author: zhangxuhong +# + +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: gq-hello-nginx + labels: + name: gq-hello-nginx + role: backend + pl: c + application: nginx + version: 1.14.0 + division: infrastructure +spec: + rules: + - host: gq-hello.juejin.im + http: + paths: + - path: / + backend: + serviceName: gq-hello-nginx + servicePort: 80 diff --git a/config/kubernetes/gq-hello-nginx-service.yaml b/config/kubernetes/gq-hello-nginx-service.yaml new file mode 100644 index 0000000..26207a4 --- /dev/null +++ b/config/kubernetes/gq-hello-nginx-service.yaml @@ -0,0 +1,24 @@ +# gq-hello-nginx-service.yaml +# +# @version 180719:1 +# @author zhangxuhong + +kind: Service +apiVersion: v1 +metadata: + name: gq-hello-nginx + labels: + name: gq-hello-nginx + role: backend + pl: c + application: nginx + version: 1.14.0 + division: infrastructure +spec: + ports: + - port: 80 + targetPort: gq-hello-nginx + protocol: TCP + name: gq-hello-nginx + selector: + name: gq-hello-nginx diff --git a/config/kubernetes/gq-hello-service.yaml b/config/kubernetes/gq-hello-service.yaml new file mode 100644 index 0000000..20464e4 --- /dev/null +++ b/config/kubernetes/gq-hello-service.yaml @@ -0,0 +1,24 @@ +# gq-hello-service.yaml +# +# @version 180719:1 +# @author zhangxuhong + +kind: Service +apiVersion: v1 +metadata: + name: gq-hello + labels: + name: gq-hello + role: backend + pl: c + application: nginx + version: 1.14.0 + division: infrastructure +spec: + ports: + - port: 9000 + targetPort: gq-hello + protocol: TCP + name: gq-hello + selector: + name: gq-hello diff --git a/config/nginx/gq-hello.public.conf b/config/nginx/gq-hello.public.conf new file mode 100644 index 0000000..2626577 --- /dev/null +++ b/config/nginx/gq-hello.public.conf @@ -0,0 +1,71 @@ +# gq-hello.public.conf +# gq-hello nginx public config. +# This api is a [public] api. +# @version 170624:8 + +# server config +server { + listen 80; + server_name gq-hello.juejin.im; + + # origin sessings + include /data/apps/nginx/conf/origin/gold.xitu.io_and_juejin.im.conf; + + default_type 'text/plain'; # or browser will download page + + root /data/repo/gq-hello/src/; + access_log /dev/stdout; + error_log /dev/stderr; + + client_body_temp_path /data/tmp/nginx/client_body_temp/ 1 2; + proxy_temp_path /data/tmp/nginx/proxy_temp/ 1 2; + fastcgi_temp_path /data/tmp/nginx/fastcgi_temp/ 1 2; + + rewrite "^(.*)/v1/sample$" $1/SampleApi.php?$query_string last; + rewrite "^(.*)/status$" $1/status.php last; + + # global location settings + location / { + return 200; + } + location = /ENV { + allow 127.0.0.1; + deny all; + } + location ~* ^/DOCUMENTS{ + return 404; + } + location ~* ^/logs{ + return 404; + } + location ~* ^/src{ + return 404; + } + location ~* ^/config{ + return 404; + } + location = /favicon.ico { + allow all; + log_not_found off; + access_log off; + } + + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + location ~ \.php$ { + if ( $fastcgi_script_name ~ \..*\/.*php ) { + return 403; + } + include fastcgi.conf; + #这里如果是本地测试,要使用docker的ip,还不能解析名称,要使用容器ip,使用hostname -I + #进入容器要使用docker exec -i -t {dockerId} /bin/sh + #查看dockerId使用命令docker ps + fastcgi_pass gq-hello:9000; + fastcgi_index index.php; + } + +} diff --git a/gq-hello-nginx.dockerfile b/gq-hello-nginx.dockerfile new file mode 100644 index 0000000..f395109 --- /dev/null +++ b/gq-hello-nginx.dockerfile @@ -0,0 +1,21 @@ +# gq-hello-nginx.dockerfile +# Dockerfile for demo gq-hello-nginx +# This docker file base on harbor02.juejin.id/lib/php:7.2.9-fpm-alpine3.8 +# @version 180719:2 +# @author zhangxuhong +# + +# base info +FROM harbor02.juejin.id/infrastructure/nginx-1.14.0-centos:latest +MAINTAINER zhangxuhong +USER root + +# copy config to /data/apps/nginx/conf/vhost/ +COPY ./config/nginx/ /data/apps/nginx/conf/vhost/ + +# define health check +HEALTHCHECK --interval=5s --timeout=3s CMD curl -fs http://127.0.0.1:80/status?src=docker_health_check -H"Host:gq-hello.juejin.im" || exit 1 + +# run php-fpm +EXPOSE 80 +ENTRYPOINT ["/data/apps/nginx/sbin/nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/gq-hello.dockerfile b/gq-hello.dockerfile new file mode 100644 index 0000000..64e6b7b --- /dev/null +++ b/gq-hello.dockerfile @@ -0,0 +1,25 @@ +# gq-hello.dockerfile +# Dockerfile for demo gq-hello +# This docker file base on harbor02.juejin.id/lib/php:7.2.9-fpm-alpine3.8 +# @version 180719:2 +# @author zhangxuhong +# + +# base info +FROM harbor02.juejin.id/infrastructure/php-7.2.9-fpm-alpine3.8:latest +MAINTAINER zhangxuhong +USER root + +# init +# RUN apk add --update --no-cache --virtual .build-deps \ + + +# copy repo to /data/repo +COPY . /data/repo/gq-hello/ + +# define health check +HEALTHCHECK --interval=5s --timeout=3s CMD netstat -an | grep 9000 > /dev/null; if [ 0 != $? ]; then exit 1; fi; + +# run php-fpm +EXPOSE 9000 +ENTRYPOINT ["php-fpm"] \ No newline at end of file diff --git a/lock/placeholder.lock b/lock/placeholder.lock new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/lock/placeholder.lock @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/SampleApi.php b/src/SampleApi.php new file mode 100644 index 0000000..2892024 --- /dev/null +++ b/src/SampleApi.php @@ -0,0 +1,18 @@ + +*/ + +define('PROCESS_NAME', 'index'); +define('DEBUG', true); + +// controller +require(dirname(__FILE__).'/protected/controller/BaseController.php'); +require(dirname(__FILE__).'/protected/controller/SampleController.php'); + +$SampleController = new SampleController; +$SampleController->run(); + diff --git a/src/protected/config/main.config.php b/src/protected/config/main.config.php new file mode 100644 index 0000000..e38f1f2 --- /dev/null +++ b/src/protected/config/main.config.php @@ -0,0 +1,50 @@ + +* +*/ + +/** + * set default ini + */ +ini_set("date.timezone", "Asia/Harbin"); + + +/** + * product config here + */ +$product = array( + 'global' => array( + 'global_id' => 'salt_fish_raw_php', + 'folder' => '/data/repo/salt_fish_raw_php/', + ), + 'log' => array( + 'open' => true, + 'address' => '/data/repo/salt_fish_raw_php/logs/', // the '/' at end of line is necessary + 'split' => 'day', // options: month, day, hour, minute + ), + 'storage' => array( + 'sample' => array( + 'host' => '192.168.0.1', + 'port' => 3306, + 'name' => 'sample', + 'charset' => 'utf8', + 'user' => 'sample', + 'pass' => 'sample', + ), + ), + 'cache' => array( + 'sample_cache' => array( + 'host' => '192.168.0.1', + 'port' => 6379, + 'pass' => null, + 'database' => 2, + 'timeout' => 0, + ), + ), + 'mongodb' => array( + 'sample' => "mongodb://sample:password@192.168.0.1:27017/sample", + ), +); diff --git a/src/protected/controller/BaseController.php b/src/protected/controller/BaseController.php new file mode 100644 index 0000000..0f26ae3 --- /dev/null +++ b/src/protected/controller/BaseController.php @@ -0,0 +1,122 @@ + +* +* @changelogs +* 160823:8 REMOVE initFeedback method. +* 160129:7 ADD checkShutDownSignal(). +* 151123:6 REFACTORY. +* 151024:5 REFACTORY. +* 150824:4 ADD isProcessNumberOverLimit(). +* 150811:3 CHANGE input method. +* 150609:2 ADD debugSwitch(). +* 150520:1 INIT baseController. +*/ + +namespace SaltFish; + +/** + * abstract class BaseController + */ +abstract class BaseController{ + + public $_log; + public $_feedback; + + /** + * [__construct description] + */ + public function __construct(){ + $this->debugSwitch(); + $this->initRequirements(); + $this->initLog(); + } + + /** + * [debugSwitch description] + * This function set debug info. + * @param null + * @return null + */ + public function debugSwitch(){ + if(!defined(DEBUG)){ + define(DEBUG, false); + } + if(DEBUG){ + ini_set('display_errors', 1); + error_reporting(E_ALL); + } + } + + /** + * [initRequirements description] + * This function load main config and all necessary files. + * @param null + * @return null + */ + public function initRequirements(){ + // init config + require(dirname(__FILE__).'/../lib/base/AutoRequire.php'); + require(dirname(__FILE__).'/../lib/base/Config.php'); + $conf = require(dirname(__FILE__).'/../config/main.config.php'); + Config::importMainConfig($conf); + // load all necessary files + $class_name = get_class($this); + $config = AutoRequire::headerArray("{$class_name}.requirements"); + AutoRequire::classes($config); + } + + /** + * [initLog description] + * This function init log address and echo settings. + * @param null + * @return null + */ + public function initLog(){ + $logConfig = Config::get('log'); + Log::setLogAddress($logConfig['address'], PROCESS_NAME, $logConfig['split']); + if(DEBUG){ + Log::setEchoSwitch(Log::ECHO_THE_LOG); + } + } + + /** + * [fetchInput description] + * @param [type] &$inputException [description] + * @return [type] [description] + */ + public function fetchInput(&$inputException){ + $inputIsOk = Input::getInputByParams( + $this->input_method, $this->input_param, $this->input, $inputException + ); + // check callback + if(!empty($input['callback'])){ + $this->output_format = Feedback::FORMAT_JSONP; + } + // gen feeedback + if(!$inputIsOk) return false; + return true; + } + + /** + * [checkShutDownSignal description] + * @param [type] $signal [description] + * @return [type] [description] + */ + public static function checkShutDownSignal($signal, $shutdownMessage = 'signal file removed'){ + if(file_exists($signal)) return; + Log::message('checkShutDownSignal', 'DETECTED', 'EXIT', $shutdownMessage); + exit("signal file removed, process shut down.\n"); + } + + /** + * [run description] + * @return [type] [description] + */ + public abstract function run(); + + +} \ No newline at end of file diff --git a/src/protected/controller/SampleController.php b/src/protected/controller/SampleController.php new file mode 100644 index 0000000..b15b81b --- /dev/null +++ b/src/protected/controller/SampleController.php @@ -0,0 +1,110 @@ + + * + * @changes + * 160901:1 INIT version. + */ + +/** + * class SampleController extends BaseController + */ +class SampleController extends SaltFish\BaseController{ + + private $_h; + public $input = array(); // user input data + + public $default_input_method = ''; + public $output_format = ''; + public $input_param = array(); + + + /** + * [__construct description] + */ + public function __construct(){ + parent::__construct(); + } + + /** + * public function setInputRules + */ + public function setInputRules(){ + $this->default_input_method = SaltFish\Input::METHOD_GET; + $this->output_format = SaltFish\Feedback::FORMAT_JSON; + $this->input_param = array( + array( + 'param' => 'uid', + 'condition' => array( + 'method' => null, + 'type' => SaltFish\Input::TYPE_INT, + 'limit' => array(), + 'default' => 0, + 'necessary' => true, + ), + ), + ); + SaltFish\Feedback::setOutputFormat($this->output_format); + SaltFish\Feedback::setCallback(null); + } + + /** + * [initDatabase description] + * @return [type] [description] + */ + public function initDatabase(){ + $this->SampleCache = new SampleCache; + $this->SampleMongoDBStorage = new SampleMongoDBStorage; + $this->SampleMySQLStorage = new SampleMySQLStorage; + } + + /** + * [run description] + * @return [type] [description] + */ + public function run(){ + // init + $Log = $this->_log; + + // get user input + $exception = array(); + $this->setInputRules(); + if(!SaltFish\Input::get($this->default_input_method, $this->input_param, $this->input, $exception)){ + $feed = SaltFish\Feedback::getFeed(FeedbackInfo::S_WRONG_INPUT, $exception, array()); + BaseView::renderJson($feed); + return false; + } + + // init request, result + $SampleRequest = new SampleRequest($this->input); + $SampleResult = new SampleResult; + + // check access token +// if(SaltFish\AccessibilityCheck::isIllegalToken($SampleRequest)){ +// $feed = SaltFish\Feedback::getFeed(FeedbackInfo::S_WRONG_INPUT, FeedbackInfo::M_ILLEGAL_TOKEN, array()); +// BaseView::renderJson($feed); +// return false; +// } + + // init database + $this->initDatabase(); + + // sample 1 : get data from cache + if(!$this->SampleCache->get($SampleRequest, $SampleResult)){ + $feed = SaltFish\Feedback::getFeed(FeedbackInfo::S_NO_RESULT, FeedbackInfo::M_NO_RESULT, array()); + BaseView::renderJson($feed); + return false; + } + + // ok, feedback + $feedData = array(); + $SampleResult->exportForFeedback($feedData); + $feed = SaltFish\Feedback::getFeed(FeedbackInfo::S_OK, FeedbackInfo::M_OK, $feedData); + BaseView::renderJson($feed); + return true; + } +} + diff --git a/src/protected/h/SampleController.requirements.h.php b/src/protected/h/SampleController.requirements.h.php new file mode 100644 index 0000000..d33d056 --- /dev/null +++ b/src/protected/h/SampleController.requirements.h.php @@ -0,0 +1,48 @@ + + * + */ + +return array( + 'config' => array( + 'main.config' + ), + 'lib' => array( + 'base/Log', + 'base/Feedback', + 'base/Input', + 'base/AccessibilityCheck', + 'network/Curl', + 'encrypt/Encrypt', + 'cache/RedisFactory', + 'cache/RedisProxy', + 'storage/File', + 'storage/FluentPDO', + 'storage/MysqlFactory', + 'storage/MysqlProxy', + 'storage/MongoDBFactory', + + + ), + 'controller' => array( + ), + 'model' => array( + 'FeedbackInfo', + 'Request', + 'Result', + 'ApiProxy', + 'SampleCache', + 'SampleMongoDBStorage', + 'SampleMySQLStorage', + 'SampleApiProxy', + 'SampleRequest', + 'SampleResult', + + ), + 'views' => array( + 'BaseView', + ), +); \ No newline at end of file diff --git a/src/protected/h/public_key.h.php b/src/protected/h/public_key.h.php new file mode 100644 index 0000000..2c78e36 --- /dev/null +++ b/src/protected/h/public_key.h.php @@ -0,0 +1,13 @@ + + * + */ + +return array( + "default" => "PV0XRWUebytdO8c9QhET", + "tester" => "kvYgJ19P3DW4mRcfNO0u", + "example" => "GBk3UAbh5SefRZsnwj2T", +); \ No newline at end of file diff --git a/src/protected/lib/algorithm/TrieTree.php b/src/protected/lib/algorithm/TrieTree.php new file mode 100644 index 0000000..ab09695 --- /dev/null +++ b/src/protected/lib/algorithm/TrieTree.php @@ -0,0 +1,256 @@ + + * + * @changes + * 150609:4 fix reference issue, ADD importTree() retval. + * 150528:3 add subContain(). + */ + +/** + * class TrieTree + */ +class TrieTree{ + + // const + const SHORT_SEARCH = true; + const NOT_SHORT_SEARCH = false; + + // properties + public $tree = array(); + + /** + * [flushTree description] + * @return [type] [description] + */ + public function flushTree(){ + $this->tree = array(); + } + + /** + * [importTree description] + * @param [type] $tree [description] + * @return [type] [description] + */ + public function importTree(&$tree){ + $this->tree = $tree; + if(empty($this->tree)) return false; + return true; + } + + /** + * [exportTree description] + * @param [type] $tree [description] + * @return [type] [description] + */ + public function exportTree(){ + return $this->tree; + } + + /** + * [insertAll description] + * @param [type] $data [description] + * @return [type] [description] + */ + public function insertAll($data){ + foreach($data as $line){ + $this->insert($line['word'], explode(",", $line['type'])); + } + } + + /** + * [insert description] + * @param [type] $utf8_str [description] + * @return [type] [description] + */ + public function insert($utf8_str = "", $type = array()){ + // unset empty type + foreach($type as $serial => $line){ + if(empty($line)){ + unset($type[$serial]); + } + } + // insert tree + $chars = String::utf8Split(trim($utf8_str)); + $chars[] = null; // null for end of thread + $count = count($chars); + $T = &$this->tree; + $last = $count-1; + for($i = 0;$i < $count;$i++){ + $c = $chars[$i]; + if(!array_key_exists($c, $T)){ + $T[$c] = array(); // insert new char + } + // fill type + if($i === $last){ + $T['__type'] = $type; + } + $T = &$T[$c]; + } + } + + /** + * [remove description] + * @param [type] $utf8_str [description] + * @return [type] [description] + */ + public function remove($utf8_str){ + $chars = String::utf8Split($utf8_str); + $chars[] = null; + if($this->_find($chars)){ // match the head char + $chars[] = null; + $count = count($chars); + $T = &$this->tree; + for($i = 0;$i < $count;$i++){ + $c = $chars[$i]; + if(count($T[$c]) == 1){ // only this thread + unset($T[$c]); + return; + } + $T = &$T[$c]; + } + } + } + + /** + * [_find description] + * @param [type] &$chars [description] + * @return [type] [description] + */ + private function _find(&$chars){ + $string = ""; + $count = count($chars); + $T = &$this->tree; + for($i = 0;$i < $count;$i++){ + $c = $chars[$i]; + if(empty($c)){ + return explode(",", $this->recrusiveStr($T, $string)); + } + $T = &$T[$c]; + $string .= $c; + } + return $string; + } + + /** + * [recrusiveStr description] + * @param [type] $tree [description] + * @param [type] $results [description] + * @return [type] [description] + */ + public function recrusiveStr($tree, $results){ + if(empty($tree)) return false; + $r = ""; + foreach($tree as $key => $subTree){ + $results .= $key; + if(empty($subTree)) return $results.","; + $r .= $this->recrusiveStr($subTree, $results); + } + return $r; + } + + /** + * [find description] + * @param [type] $utf8_str [description] + * @return [type] [description] + */ + public function find($utf8_str){ + $chars = String::utf8Split($utf8_str); + $chars[] = null; + return $this->_find($chars); + } + + /** + * [subContain description] + * @param [type] &$chars [description] + * @param [type] $len [description] + * @param [type] &$Tree [description] + * @param [type] &$hit_tmp [description] + * @param [type] &$hit_words [description] + * @param [type] $short_search [description] + * @return [type] [description] + */ + private static function subContain(&$chars, $len, &$Tree, &$hit_tmp, &$hit_words, $short_search){ + for($i = 0; $i<$len; $i++){ + $c = $chars[$i]; + if(array_key_exists($c, $Tree)){ + $T = &$Tree[$c]; + for($j = $i + 1;$j < $len;$j++){ + $c = $chars[$j]; + $hit_tmp[] = $chars[$j-1]; + // if end match + if(array_key_exists(null, $T)){ + $hitWord = implode("", $hit_tmp); + if(!empty($hit_words[$hitWord])){ + $hit_words[$hitWord]['hits'] ++; + }else{ + $hit_words[$hitWord] = array( + 'hits' => 1, + 'type' => $T['__type'], + ); + } + if($short_search){ + $hit_tmp = array(); + return true; + } + } + // if wildcard + if(array_key_exists(" ", $T)){ + $T = &$T[" "]; + $hit_tmp[] = " "; + self::subContain($chars, $len, $T, $hit_tmp, $hit_words, true); + $hit_tmp = array(); + continue; + } + // if miss match + if(!array_key_exists($c, $T)){ + array_pop($hit_tmp); + break; + } + $T = &$T[$c]; + } + } + } + } + + /** + * [contain description] + * @param [type] $utf8_str [description] + * @param integer $do_count [description] + * @return [type] [description] + */ + public function contain($utf8_str, &$hit_words = array(), $short_search = false){ + $chars = String::utf8Split($utf8_str); + $chars[] = null; + $len = count($chars); + $Tree = &$this->tree; + $count = 0; + $totalLoop = 0; + $hit_tmp = array(); + self::subContain($chars, $len, $Tree, $hit_tmp, $hit_words, $short_search); + if(!empty($hit_words)){ + return true; + }else{ + return false; + } + } + + /** + * [containAll description] + * @param [type] $str_array [description] + * @return [type] [description] + */ + public function containAll($str_array){ + foreach($str_array as $str){ + if($this->contain($str)){ + return true; + } + } + return false; + } + +} + diff --git a/src/protected/lib/base/AccessibilityCheck.php b/src/protected/lib/base/AccessibilityCheck.php new file mode 100644 index 0000000..b3a0dc5 --- /dev/null +++ b/src/protected/lib/base/AccessibilityCheck.php @@ -0,0 +1,132 @@ + + * + * @changelogs + * 160908:4 ADD generatePayload(). + * 160823:3 CHANGE string serialize method. + * 151024:2 REFACTORY + * 151020:1 INIT version. + */ + +namespace SaltFish; + +/** + * class AccessibilityCheck + */ +class AccessibilityCheck{ + + const PUBLIC_KEY_FILE = 'public_key'; + const DEFAULT_PUBLIC_KEY = 'default'; + const DEFAULT_EXPIRE = 3; // minutes + + private static $instance; + private $publicKey = ""; + + /** + * [__construct description] + */ + private function __construct(){ + $this->loadHeaderFile(); + } + + /** + * [getInstance description] + * @param null + * @return Log $instance + */ + public static function getInstance(){ + if(empty(self::$instance)){ + self::$instance = new AccessibilityCheck; + } + return self::$instance; + } + + /** + * [loadHeaderFile description] + * @return [type] [description] + */ + public function loadHeaderFile(){ + $this->publicKey = AutoRequire::headerArray(self::PUBLIC_KEY_FILE); + return true; + } + + /** + * [getPublicKey description] + * @param [type] $src [description] + * @return [type] [description] + */ + public static function getPublicKey($src){ + $instance = self::getInstance(); + if(empty($instance->publicKey[$src])) return $instance->publicKey[self::DEFAULT_PUBLIC_KEY]; + return $instance->publicKey[$src]; + } + + /** + * [generatePayload description] + * @param [type] $data [description] + * @param [type] $public_key [description] + * @param integer $expire [description] + * @return [type] [description] + */ + public static function generatePayload($data, $public_key, $expire = self::DEFAULT_EXPIRE){ + ksort($data); + $str = ""; + $str .= json_encode($data); + $str .= $public_key; + $payloads = array(); + if(empty($expire)){ + return array( + 0 => $str, + ); + } + $start = -floor($expire * 0.5); + for($i=$start;$i<=-$start;$i++){ + if($i!==0){ + $payloads[] = $str.strtotime(date("Y-m-d H:i:00", strtotime("{$i} minute"))); + }else{ + $payloads[] = $str.strtotime(date("Y-m-d H:i:00")); + } + } + return $payloads; + } + + /** + * [generateToken description] + * @param [type] $data [description] + * @param [type] $public_key [description] + * @return [type] [description] + */ + public static function generateToken($data, $public_key){ + ksort($data); + $str = ""; + $str .= json_encode($data); + $str .= $public_key; + $str .= strtotime(date("Y-m-d H:i:00")); + return Encrypt::hash($str); + } + + /** + * [isIllegalToken description] + * @param Request $Request [description] + * @return boolean [description] + */ + public static function isIllegalToken(\Request $Request){ + $instance = self::getInstance(); + $data = array(); + $token = ""; + $src = ""; + $Request->exportForAccessibilityCheck($data); + $token = $Request->getToken(); + $src = $Request->getSrc(); + $payloads = self::generatePayload($data, self::getPublicKey($src)); + foreach($payloads as $payload){ + if(Encrypt::matchMd5($payload, $token)) return false; + } + return true; + } + +} \ No newline at end of file diff --git a/src/protected/lib/base/AutoRequire.php b/src/protected/lib/base/AutoRequire.php new file mode 100644 index 0000000..737eb90 --- /dev/null +++ b/src/protected/lib/base/AutoRequire.php @@ -0,0 +1,67 @@ + + * + * @changelogs + * 151123:3 CHNAGE function name. + * 150609:2 FIX _header_array(), _header_json() require file name issue. + * 150520:1 INIT commit. + */ + +namespace SaltFish; + +/** + * CLASS AutoRequire + */ +class AutoRequire{ + + /** + * [basedir description] + * @param null + * @return string basedir + */ + private static function basedir(){ + return dirname(__FILE__).'/../../'; + } + + /** + * [classes description] + * @param array $classes, required class type and name + * @return null + */ + public static function classes( + $classes = array('type' => array('name_1', 'name_2')) + ){ + $basedir = self::basedir(); + foreach($classes as $type => $names){ + if(empty($names)) continue; + foreach($names as $name){ + require("{$basedir}{$type}/{$name}.php"); + } + } + } + + /** + * [headerArray description] + * @param string $name, config name + * @return array h, in file array + */ + public static function headerArray($name = ''){ + $basedir = self::basedir(); + return require("{$basedir}h/{$name}.h.php"); + } + + /** + * [headerJson description] + * @param string $name, config name + * @return array h, in file array + */ + public static function headerJson($name = ''){ + $basedir = self::basedir(); + return json_decode(require("{$basedir}h/{$name}.h.json"), true); + } + +} \ No newline at end of file diff --git a/src/protected/lib/base/Config.php b/src/protected/lib/base/Config.php new file mode 100644 index 0000000..8a1785d --- /dev/null +++ b/src/protected/lib/base/Config.php @@ -0,0 +1,72 @@ + + * + * @changes + * 151123:1 INIT commit. + */ + +namespace SaltFish; + +/** + * CLASS Config + */ +class Config{ + + private static $instance; + private $main; + private $h; + + private function __construct(){ + + } + + /** + * [importMainConfig description] + * @param array $main, main config array + * @return null + */ + public static function importMainConfig(&$main){ + $instance = self::getInstance(); + $instance->main = &$main; + } + + /** + * [get description] + * @param string $field_name, main config field name + * @return array config + */ + public static function get($field_name = ''){ + $instance = self::getInstance(); + return $instance->main[$field_name]; + } + + /** + * [getHeader description] + * @param string $name, header file name + * @return array h, header array + */ + public static function getHeader($name = ''){ + $instance = self::getInstance(); + if(empty($instance->h[$name])){ + $instance->h[$name] = AutoRequire::headerArray($name); + } + return $instance->h[$name]; + } + + /** + * [getInstance description] + * @param null + * @return Config $instance + */ + public static function getInstance(){ + if(empty(self::$instance)){ + self::$instance = new Config; + } + return self::$instance; + } + +} \ No newline at end of file diff --git a/src/protected/lib/base/Feedback.php b/src/protected/lib/base/Feedback.php new file mode 100644 index 0000000..1c82980 --- /dev/null +++ b/src/protected/lib/base/Feedback.php @@ -0,0 +1,150 @@ + +* +* @changelogs +* 160823:10 CHANGE to singleton. +* 151024:9 REFACTORY. +* 150811:8 ADD raw result format. +* 150520:7 old version. +* +*/ + +namespace SaltFish; + +/** + * class Feedback + */ +class Feedback{ + + // const + const SIGNAL = 's'; + const MESSAGE = 'm'; + const DATA = 'd'; + const M_OK = 'ok'; + const FORMAT_RAW = 'raw'; + const FORMAT_JSON = 'json'; + const FORMAT_JSONP = 'jsonp'; + const FORMAT_SERIALIZE = 'serialize'; + + private static $instance; + public $output_format = self::FORMAT_JSON; + public $callback = ''; + + public static $result_tmpl = array( + self::SIGNAL => false, + self::MESSAGE => self::M_OK, + self::DATA => array(), + ); + + + /** + * [__construct description] + */ + private function __construct(){ + return; + } + + /** + * [getInstance description] + * @param null + * @return Log $instance + */ + public static function getInstance(){ + if(empty(self::$instance)){ + self::$instance = new Feedback; + } + return self::$instance; + } + + /** + * [setOutputFormat description] + * @param [type] $output_format [description] + */ + public static function setOutputFormat($output_format = self::FORMAT_JSON){ + $instance = self::getInstance(); + $instance->output_format = $output_format; + return; + } + + /** + * [setCallback description] + * @param string $callback [description] + */ + public static function setCallback($callback = ''){ + $instance = self::getInstance(); + $instance->callback = $callback; + return; + } + + /** + * [resultTransfer description] + * You can define your return format status field in here + * @param [type] $result [description] + * @return [type] [description] + */ + private static function resultTransfer($result){ + if(is_numeric($result)) return $result; + if(!empty($result)){ + return true; + }else{ + return false; + } + } + + /** + * [returnFromat description] + * this method leave format param for extension on future + * @param [type] $data [description] + * @param [type] $format [description] + * @param string $callback [description] + * @return [type] [description] + */ + private static function returnFromat( + &$data, $format = self::FORMAT_JSON, $callback = '' + ){ + if($format === self::FORMAT_JSON){ + return json_encode($data); + }elseif($format === self::FORMAT_JSONP||!empty($callback)){ + $data = json_encode($data); + $callback = urlencode($callback); + return "{$callback}($data)"; + }elseif($format === self::FORMAT_SERIALIZE){ + return serialize($data); + }elseif($format === self::FORMAT_RAW){ + return $data; + } + } + + /** + * [getFeed description] + * @param boolean $result [description] + * @param array $message [description] + * @param array $data [description] + * @param [type] $format [description] + * @param string $callback [description] + * @return [type] [description] + */ + public static function getFeed( + $result = false, + $message = array(), + $data = array() + ){ + $instance = self::getInstance(); + $result = self::resultTransfer($result); + $r = self::$result_tmpl; + $r[self::SIGNAL] = $result; + if(!empty($message)){ + $r[self::MESSAGE] = $message; + } + if(!empty($data)){ + $r[self::DATA] = &$data; + } + return self::returnFromat($r, $instance->output_format, $instance->callback); + } + + +} diff --git a/src/protected/lib/base/Input.php b/src/protected/lib/base/Input.php new file mode 100644 index 0000000..2bf82ef --- /dev/null +++ b/src/protected/lib/base/Input.php @@ -0,0 +1,155 @@ + +*/ + +namespace SaltFish; + +/** + * class Input + */ +class Input{ + + // input method + const METHOD_GET = 'get'; + const METHOD_POST = 'post'; + const METHOD_ARGV = 'argv'; + const METHOD_HTTP = 'http'; + // input types + const TYPE_INT = 'int'; + const TYPE_STRING = 'string'; + const TYPE_ENUM = 'enum'; + const TYPE_ARRAY = 'array'; + const TYPE_BOOLEAN = 'array'; + + /** + * [getInputByMethod description] + * get and fill default value when input empty. + * @param [type] $method [description] + * @param [type] $param [description] + * @param [type] $default [description] + * @return mixed, input data + */ + private static function getInputByMethod($method, $param, $default){ + if($method === self::METHOD_GET){ + return empty($_GET[$param]) ? $default : $_GET[$param]; + }elseif($method === self::METHOD_POST){ + return empty($_POST[$param]) ? $default : $_POST[$param]; + }elseif($method === self::METHOD_HTTP){ + $post = empty($_POST[$param]) ? null : $_POST[$param]; + if(empty($post)){ + return empty($_GET[$param]) ? $default : $_GET[$param]; + } + return $post; + }elseif($method === self::METHOD_ARGV){ + $argvInput = getopt(false, array("{$param}:")); + return empty($argvInput[$param]) ? $default : $argvInput[$param]; + } + } + + /** + * [isMissing description] + * @param [type] &$data [description] + * @param [type] $necessary [description] + * @return boolean [description] + */ + private static function isMissing(&$data, $necessary){ + if($necessary && !isset($data)) return true; + return false; + } + + /** + * [isIllegal description] + * @param [type] &$data [description] + * @param [type] &$condition [description] + * @return boolean [description] + */ + private static function isIllegal(&$data, &$condition){ + $type = $condition['type']; + if($type === self::TYPE_INT){ + $data = (int)$data; + if(!empty($condition['limit'])){ + if(isset($condition['limit']['min'])){ + if($data < $condition['limit']['min']) return true; + } + if(isset($condition['limit']['max'])){ + if($data > $condition['limit']['max']) return true; + } + } + }elseif($type === self::TYPE_STRING){ + if(!empty($condition['limit'])){ + $len = strlen($data); + if(isset($condition['limit']['min'])){ + if($len < $condition['limit']['min']) return true; + } + if(isset($condition['limit']['max'])){ + if($len > $condition['limit']['max']) return true; + } + } + }elseif($type === self::TYPE_ENUM){ + if(!empty($condition['options'])){ + $enums = array(); + foreach($condition['options'] as $avaliable_value){ + $enums[$avaliable_value] = true; + } + if(empty($enums[$data])) return true; + } + }elseif($type === self::TYPE_ARRAY){ + $subCondition = $condition; + $subCondition['type'] = $condition['sub_type']; + if(self::isIllegal($line, $subCondition)) return true; + }elseif($type === self::TYPE_BOOLEAN){ + if(!isset($ata)) return true; + } + return false; + } + + /** + * [get description] + * @param [type] $default_input_method [description] + * @param array &$input_param [description] + * @param array &$result [description] + * @param array &$exception [description] + * @return [type] [description] + */ + public static function get( + $default_input_method = self::METHOD_HTTP, + &$input_param = array(), + &$result = array(), + &$exception = array() + ){ + $isMissing = false; + $isIllegal = false; + $exception = array( + 'missing' => array(), + 'illegal' => array(), + ); + foreach($input_param as $line){ + $param = &$line['param']; + $condition = &$line['condition']; + // check method + $method = empty($condition['method']) ? $default_input_method : $condition['method']; + // get and fill default value when input empty. + $result[$param] = self::getInputByMethod($method, $param, $condition['default']); + // check necessary + if(self::isMissing($result[$param], $condition['necessary'])){ + $isMissing = true; + $exception['missing'] = $param; + } + // check illegal + if(self::isIllegal($result[$param], $condition)){ + $isIllegal = true; + $exception['illegal'] = $param; + } + } + if($isMissing||$isIllegal) return false; + return true; + } + +} + + + diff --git a/src/protected/lib/base/Log.php b/src/protected/lib/base/Log.php new file mode 100644 index 0000000..220ec2e --- /dev/null +++ b/src/protected/lib/base/Log.php @@ -0,0 +1,198 @@ + +* +* @changelogs 151231:8 ADD writeRaw(). +* 151123:7 ADD getInstance(). +* 151024:6 RENAME. +* 150831:5 ADD process name. +* 150826:4 ADD uniqid +* 150609:3 ADD echoSwitch(). +* 140626:2 INIT base log. +* +* @example +* +* exec: +* Log::error("somefunction()", "failed to exec", "jump", array('return' => false)); +* then will write to log: +* 0000-00-00 00:00:00 <-ERROR-> [somefunction()] [failed to exec] [jump] return: false \n +*/ + +namespace SaltFish; + +/** + * class Log + */ +class Log{ + + const SIGNAL_OK = ' O K '; + const SIGNAL_BAD = ' BAD '; + const SIGNAL_MESSAGE = 'MESSAGE'; + const SIGNAL_WARING = 'WARING!'; + const SIGNAL_ERROR = '-ERROR-'; + const SIGNAL_FATAL = '-FATAL-'; + + const ECHO_THE_LOG = true; + const DO_NOT_ECHO_THE_LOG = false; + + private static $instance; + private $logAddress = ''; + private $echoSwitch = false; + + private function __construct(){ + + } + + /** + * [getInstance description] + * @param null + * @return Log $instance + */ + public static function getInstance(){ + if(empty(self::$instance)){ + self::$instance = new Log; + } + return self::$instance; + } + + /** + * [setLogAddress description] + * @param string $addr + * @param string $logName + * @param string $split, log split method + */ + public static function setLogAddress($addr = '', $logName = '', $split = ''){ + $instance = self::getInstance(); + if($split === 'year'){ + $suffix = date("Y"); + }elseif($split === 'month'){ + $suffix = date("Ym"); + }elseif($split === 'day'){ + $suffix = date("Ymd"); + }elseif($split === 'hour'){ + $suffix = date("YmdH"); + }elseif($split === 'minute'){ + $suffix = date("YmdHi"); + }elseif($split === 'second'){ + $suffix = date("YmdHis"); + }else{ + $suffix = ''; + } + $instance->logAddress = $addr.$logName.".".$suffix.".".uniqid().".log"; + } + + /** + * [setEchoSwitch description] + * @param boolean $switch, set true to print log + */ + public static function setEchoSwitch($switch = false){ + $instance = self::getInstance(); + $instance->echoSwitch = $switch; + } + + /** + * [writeLog description] + * @param [type] $signal [description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + private static function writeLog($signal, $action = '', $ret_info = '', $process = '', $dump = array()){ + $instance = self::getInstance(); + $timestamp = date("Y-m-d H:i:s"); + $dump = serialize($dump); + error_log("{$timestamp} <{$signal}>\t[{$action}]\t[{$ret_info}][{$process}] {$dump} \n", 3, $instance->logAddress); + if($instance->echoSwitch){ + echo("{$timestamp} <{$signal}>\t[{$action}]\t[{$ret_info}][{$process}] {$dump} \n"); + } + } + + /** + * [writeRaw description] + * @param [type] &$line [description] + * @return [type] [description] + */ + public static function writeRaw(&$line){ + $instance = self::getInstance(); + error_log($line+"\n", 3, $instance->logAddress); + } + + /** + * [ok description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function ok($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_OK , $action, $ret_info, $process, $dump); + } + + /** + * [bad description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function bad($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_BAD , $action, $ret_info, $process, $dump); + } + + /** + * [message description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function message($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_MESSAGE , $action, $ret_info, $process, $dump); + } + + /** + * [waring description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function waring($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_WARING , $action, $ret_info, $process, $dump); + } + + /** + * [error description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function error($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_ERROR , $action, $ret_info, $process, $dump); + } + + /** + * [fatal description] + * @param string $action [description] + * @param string $ret_info [description] + * @param string $process [description] + * @param array $dump [description] + * @return [type] [description] + */ + public static function fatal($action = '', $ret_info = '', $process = '', $dump = array()){ + self::writeLog(self::SIGNAL_FATAL , $action, $ret_info, $process, $dump); + exit(); + } + +} diff --git a/src/protected/lib/base/ObjectArray.php b/src/protected/lib/base/ObjectArray.php new file mode 100644 index 0000000..aa6cddf --- /dev/null +++ b/src/protected/lib/base/ObjectArray.php @@ -0,0 +1,130 @@ + + * + * @changelogs + * 151201:1 INIT version. + */ + +namespace SaltFish; + +/** + * abstract class ObjectArray implements ArrayAccess, Iterator + */ +abstract class ObjectArray implements ArrayAccess, Iterator{ + + private $valid = false; + private $data; + + + /** + * [__construct description] + * @param array &$data [description] + */ + public function __construct(&$data = array()){ + $this->import($data); + } + + /** + * [import description] + * @param array &$data [description] + * @return [type] [description] + */ + public function import(&$data = array()){ + $this->data = $data; + } + + /** + * [exportRaw description] + * @param [type] &$data [description] + * @return [type] [description] + */ + public function exportRaw(&$data){ + $data = $this->data; + } + + /** + * [offsetSet description] + * @param [type] $offset [description] + * @param [type] $value [description] + * @return [type] [description] + */ + public function offsetSet($offset, $value){ + if(is_null($offset)){ + $this->data[] = $value; + }else{ + $this->data[$offset] = $value; + } + } + + /** + * [offsetExists description] + * @param [type] $offset [description] + * @return [type] [description] + */ + public function offsetExists($offset){ + return isset($this->data[$offset]); + } + + /** + * [offsetUnset description] + * @param [type] $offset [description] + * @return [type] [description] + */ + public function offsetUnset($offset){ + unset($this->data[$offset]); + } + + /** + * [offsetGet description] + * @param [type] $offset [description] + * @return [type] [description] + */ + public function offsetGet($offset){ + return isset($this->data[$offset]) ? $this->data[$offset] : null; + } + + /** + * [rewind description] + * @return [type] [description] + */ + public function rewind(){ + $this->valid = (false !== reset($this->data)); + } + + /** + * [current description] + * @return [type] [description] + */ + public function current(){ + return current($this->data); + } + + /** + * [key description] + * @return [type] [description] + */ + public function key(){ + return key($this->data); + } + + /** + * [next description] + * @return function [description] + */ + public function next(){ + $this->valid = (false !== next($this->data)); + } + + /** + * [valid description] + * @return [type] [description] + */ + public function valid(){ + return $this->valid; + } + +} \ No newline at end of file diff --git a/src/protected/lib/cache/Predis_Client.php b/src/protected/lib/cache/Predis_Client.php new file mode 100644 index 0000000..90e7d0e --- /dev/null +++ b/src/protected/lib/cache/Predis_Client.php @@ -0,0 +1,3484 @@ +getMessage()); + } +} + +// Communication errors +class Predis_CommunicationException extends PredisException { + private $_connection; + + public function __construct(Predis_Connection $connection, $message = null, $code = null) { + $this->_connection = $connection; + parent::__construct($message, $code); + } + + public function getConnection() { return $this->_connection; } + public function shouldResetConnection() { return true; } +} + +// Unexpected responses +class Predis_MalformedServerResponse extends Predis_CommunicationException { } + +/* ------------------------------------------------------------------------- */ + +class Predis_Client { + const VERSION = '0.6.6'; + private $_options, $_connection, $_serverProfile, $_responseReader; + + public function __construct($parameters = null, $clientOptions = null) { + $this->setupClient($clientOptions !== null ? $clientOptions : new Predis_ClientOptions()); + $this->setupConnection($parameters); + } + + public static function create(/* arguments */) { + $argv = func_get_args(); + $argc = func_num_args(); + + $options = null; + $lastArg = $argv[$argc-1]; + if ($argc > 0 && !is_string($lastArg) && ($lastArg instanceof Predis_ClientOptions || + is_subclass_of($lastArg, 'Predis_RedisServerProfile'))) { + $options = array_pop($argv); + $argc--; + } + + if ($argc === 0) { + throw new Predis_ClientException('Missing connection parameters'); + } + + return new Predis_Client($argc === 1 ? $argv[0] : $argv, $options); + } + + private static function filterClientOptions($options) { + if ($options instanceof Predis_ClientOptions) { + return $options; + } + if (is_array($options)) { + return new Predis_ClientOptions($options); + } + if ($options instanceof Predis_RedisServerProfile) { + return new Predis_ClientOptions(array( + 'profile' => $options + )); + } + if (is_string($options)) { + return new Predis_ClientOptions(array( + 'profile' => Predis_RedisServerProfile::get($options) + )); + } + throw new InvalidArgumentException("Invalid type for client options"); + } + + private function setupClient($options) { + $options = self::filterClientOptions($options); + + $this->setProfile($options->profile); + + $reader = $options->reader; + $reader->setOption('iterable_multibulk', $options->iterable_multibulk); + $reader->setOption('throw_on_error', $options->throw_on_error); + + $this->_options = $options; + $this->_responseReader = $reader; + } + + private function setupConnection($parameters) { + if ($parameters !== null && !(is_array($parameters) || is_string($parameters))) { + throw new Predis_ClientException('Invalid parameters type (array or string expected)'); + } + if (is_array($parameters) && isset($parameters[0])) { + $cluster = new Predis_ConnectionCluster($this->_options->key_distribution); + foreach ($parameters as $shardParams) { + $cluster->add($this->createConnection($shardParams)); + } + $this->setConnection($cluster); + } + else { + $this->setConnection($this->createConnection($parameters)); + } + } + + private function createConnection($parameters) { + if (!$parameters instanceof Predis_ConnectionParameters) { + $parameters = new Predis_ConnectionParameters($parameters); + } + + $connection = new Predis_Connection($parameters, $this->_responseReader); + if ($parameters->password !== null) { + $connection->pushInitCommand($this->createCommand( + 'auth', array($parameters->password) + )); + } + if ($parameters->database !== null) { + $connection->pushInitCommand($this->createCommand( + 'select', array($parameters->database) + )); + } + + return $connection; + } + + private function setConnection(Predis_IConnection $connection) { + $this->_connection = $connection; + } + + public function setProfile($serverProfile) { + if ($serverProfile instanceof Predis_RedisServerProfile) { + $this->_serverProfile = $serverProfile; + } + else if (is_string($serverProfile)) { + $this->_serverProfile = Predis_RedisServerProfile::get($serverProfile); + } + else { + throw new InvalidArgumentException( + "Invalid type for server profile, Predis_RedisServerProfile or string expected" + ); + } + } + + public function getProfile() { + return $this->_serverProfile; + } + + public function getResponseReader() { + return $this->_responseReader; + } + + public function getClientFor($connectionAlias) { + if (!Predis_Shared_Utils::isCluster($this->_connection)) { + throw new Predis_ClientException( + 'This method is supported only when the client is connected to a cluster of connections' + ); + } + + $connection = $this->_connection->getConnectionById($connectionAlias); + if ($connection === null) { + throw new InvalidArgumentException( + "Invalid connection alias: '$connectionAlias'" + ); + } + + $newClient = new Predis_Client(); + $newClient->setupClient($this->_options); + $newClient->setConnection($connection); + return $newClient; + } + + public function connect() { + $this->_connection->connect(); + } + + public function disconnect() { + $this->_connection->disconnect(); + } + + public function isConnected() { + return $this->_connection->isConnected(); + } + + public function getConnection($id = null) { + if (!isset($id)) { + return $this->_connection; + } + else { + return Predis_Shared_Utils::isCluster($this->_connection) + ? $this->_connection->getConnectionById($id) + : $this->_connection; + } + } + + public function __call($method, $arguments) { + $command = $this->_serverProfile->createCommand($method, $arguments); + return $this->_connection->executeCommand($command); + } + + public function createCommand($method, $arguments = array()) { + return $this->_serverProfile->createCommand($method, $arguments); + } + + public function executeCommand(Predis_Command $command) { + return $this->_connection->executeCommand($command); + } + + public function executeCommandOnShards(Predis_Command $command) { + $replies = array(); + if (Predis_Shared_Utils::isCluster($this->_connection)) { + foreach($this->_connection as $connection) { + $replies[] = $connection->executeCommand($command); + } + } + else { + $replies[] = $this->_connection->executeCommand($command); + } + return $replies; + } + + public function rawCommand($rawCommandData, $closesConnection = false) { + if (Predis_Shared_Utils::isCluster($this->_connection)) { + throw new Predis_ClientException('Cannot send raw commands when connected to a cluster of Redis servers'); + } + return $this->_connection->rawCommand($rawCommandData, $closesConnection); + } + + private function sharedInitializer($argv, $initializer) { + $argc = count($argv); + if ($argc === 0) { + return $this->$initializer(); + } + else if ($argc === 1) { + list($arg0) = $argv; + return is_array($arg0) ? $this->$initializer($arg0) : $this->$initializer(null, $arg0); + } + else if ($argc === 2) { + list($arg0, $arg1) = $argv; + return $this->$initializer($arg0, $arg1); + } + return $this->$initializer($this, $arguments); + } + + public function pipeline(/* arguments */) { + $args = func_get_args(); + return $this->sharedInitializer($args, 'initPipeline'); + } + + public function pipelineSafe($pipelineBlock = null) { + return $this->initPipeline(array('safe' => true), $pipelineBlock); + } + + private function initPipeline(Array $options = null, $pipelineBlock = null) { + $pipeline = null; + if (isset($options)) { + if (isset($options['safe']) && $options['safe'] == true) { + $connection = $this->getConnection(); + $pipeline = new Predis_CommandPipeline($this, $connection instanceof Predis_Connection + ? new Predis_Pipeline_SafeExecutor($connection) + : new Predis_Pipeline_SafeClusterExecutor($connection) + ); + } + else { + $pipeline = new Predis_CommandPipeline($this); + } + } + else { + $pipeline = new Predis_CommandPipeline($this); + } + return $this->pipelineExecute($pipeline, $pipelineBlock); + } + + private function pipelineExecute(Predis_CommandPipeline $pipeline, $block) { + return $block !== null ? $pipeline->execute($block) : $pipeline; + } + + public function multiExec(/* arguments */) { + $args = func_get_args(); + return $this->sharedInitializer($args, 'initMultiExec'); + } + + private function initMultiExec(Array $options = null, $transBlock = null) { + $multi = isset($options) ? new Predis_MultiExecBlock($this, $options) : new Predis_MultiExecBlock($this); + return $transBlock !== null ? $multi->execute($transBlock) : $multi; + } + + public function pubSubContext(Array $options = null) { + return new Predis_PubSubContext($this, $options); + } +} + +/* ------------------------------------------------------------------------- */ + +interface Predis_IClientOptionsHandler { + public function validate($option, $value); + public function getDefault(); +} + +class Predis_ClientOptionsProfile implements Predis_IClientOptionsHandler { + public function validate($option, $value) { + if ($value instanceof Predis_RedisServerProfile) { + return $value; + } + if (is_string($value)) { + return Predis_RedisServerProfile::get($value); + } + throw new InvalidArgumentException("Invalid value for option $option"); + } + + public function getDefault() { + return Predis_RedisServerProfile::getDefault(); + } +} + +class Predis_ClientOptionsKeyDistribution implements Predis_IClientOptionsHandler { + public function validate($option, $value) { + if ($value instanceof Predis_Distribution_IDistributionStrategy) { + return $value; + } + if (is_string($value)) { + $valueReflection = new ReflectionClass($value); + if ($valueReflection->isSubclassOf('Predis_Distribution_IDistributionStrategy')) { + return new $value; + } + } + throw new InvalidArgumentException("Invalid value for option $option"); + } + + public function getDefault() { + return new Predis_Distribution_HashRing(); + } +} + +class Predis_ClientOptionsIterableMultiBulk implements Predis_IClientOptionsHandler { + public function validate($option, $value) { + return (bool) $value; + } + + public function getDefault() { + return false; + } +} + +class Predis_ClientOptionsThrowOnError implements Predis_IClientOptionsHandler { + public function validate($option, $value) { + return (bool) $value; + } + + public function getDefault() { + return true; + } +} + +class Predis_ClientOptionsReader implements Predis_IClientOptionsHandler { + public function validate($option, $value) { + if ($value instanceof Predis_IResponseReader) { + return $value; + } + if (is_string($value)) { + if ($value === 'composable') { + return new Predis_ResponseReader(); + } + $valueReflection = new ReflectionClass($value); + if ($valueReflection->isSubclassOf('Predis_IResponseReader')) { + return new $value; + } + } + throw new InvalidArgumentException("Invalid value for option $option"); + } + + public function getDefault() { + return new Predis_FastResponseReader(); + } +} + +class Predis_ClientOptions { + private static $_optionsHandlers; + private $_options; + + public function __construct($options = null) { + self::initializeOptionsHandlers(); + $this->initializeOptions($options !== null ? $options : array()); + } + + private static function initializeOptionsHandlers() { + if (!isset(self::$_optionsHandlers)) { + self::$_optionsHandlers = self::getOptionsHandlers(); + } + } + + private static function getOptionsHandlers() { + return array( + 'profile' => new Predis_ClientOptionsProfile(), + 'key_distribution' => new Predis_ClientOptionsKeyDistribution(), + 'iterable_multibulk' => new Predis_ClientOptionsIterableMultiBulk(), + 'throw_on_error' => new Predis_ClientOptionsThrowOnError(), + 'reader' => new Predis_ClientOptionsReader(), + ); + } + + private function initializeOptions($options) { + foreach ($options as $option => $value) { + if (isset(self::$_optionsHandlers[$option])) { + $handler = self::$_optionsHandlers[$option]; + $this->_options[$option] = $handler->validate($option, $value); + } + } + } + + public function __get($option) { + if (!isset($this->_options[$option])) { + $defaultValue = self::$_optionsHandlers[$option]->getDefault(); + $this->_options[$option] = $defaultValue; + } + return $this->_options[$option]; + } + + public function __isset($option) { + return isset(self::$_optionsHandlers[$option]); + } +} + +/* ------------------------------------------------------------------------- */ + +class Predis_Protocol { + const NEWLINE = "\r\n"; + const OK = 'OK'; + const ERROR = 'ERR'; + const QUEUED = 'QUEUED'; + const NULL = 'nil'; + + const PREFIX_STATUS = '+'; + const PREFIX_ERROR = '-'; + const PREFIX_INTEGER = ':'; + const PREFIX_BULK = '$'; + const PREFIX_MULTI_BULK = '*'; +} + +abstract class Predis_Command { + private $_hash; + private $_arguments = array(); + + public abstract function getCommandId(); + + public abstract function serializeRequest($command, $arguments); + + public function canBeHashed() { + return true; + } + + public function getHash(Predis_Distribution_IDistributionStrategy $distributor) { + if (isset($this->_hash)) { + return $this->_hash; + } + + if (isset($this->_arguments[0])) { + // TODO: should we throw an exception if the command does + // not support sharding? + $key = $this->_arguments[0]; + + $start = strpos($key, '{'); + if ($start !== false) { + $end = strpos($key, '}', $start); + if ($end !== false) { + $key = substr($key, ++$start, $end - $start); + } + } + + $this->_hash = $distributor->generateKey($key); + return $this->_hash; + } + + return null; + } + + public function closesConnection() { + return false; + } + + protected function filterArguments(Array $arguments) { + return $arguments; + } + + public function setArguments(/* arguments */) { + $this->_arguments = $this->filterArguments(func_get_args()); + unset($this->_hash); + } + + public function setArgumentsArray(Array $arguments) { + $this->_arguments = $this->filterArguments($arguments); + unset($this->_hash); + } + + public function getArguments() { + return $this->_arguments; + } + + public function getArgument($index = 0) { + if (isset($this->_arguments[$index]) === true) { + return $this->_arguments[$index]; + } + } + + public function parseResponse($data) { + return $data; + } + + public final function invoke() { + return $this->serializeRequest($this->getCommandId(), $this->getArguments()); + } +} + +abstract class Predis_InlineCommand extends Predis_Command { + public function serializeRequest($command, $arguments) { + if (isset($arguments[0]) && is_array($arguments[0])) { + $arguments[0] = implode($arguments[0], ' '); + } + return $command . (count($arguments) > 0 + ? ' ' . implode($arguments, ' ') . "\r\n" : "\r\n" + ); + } +} + +abstract class Predis_BulkCommand extends Predis_Command { + public function serializeRequest($command, $arguments) { + $data = array_pop($arguments); + if (is_array($data)) { + $data = implode($data, ' '); + } + return $command . ' ' . implode($arguments, ' ') . ' ' . strlen($data) . + "\r\n" . $data . "\r\n"; + } +} + +abstract class Predis_MultiBulkCommand extends Predis_Command { + public function serializeRequest($command, $arguments) { + $cmd_args = null; + $argsc = count($arguments); + + if ($argsc === 1 && is_array($arguments[0])) { + $cmd_args = $arguments[0]; + $argsc = count($cmd_args); + } + else { + $cmd_args = $arguments; + } + + $cmdlen = strlen($command); + $reqlen = $argsc + 1; + + $buffer = "*{$reqlen}\r\n\${$cmdlen}\r\n{$command}\r\n"; + for ($i = 0; $i < $reqlen - 1; $i++) { + $argument = $cmd_args[$i]; + $arglen = strlen($argument); + $buffer .= "\${$arglen}\r\n{$argument}\r\n"; + } + + return $buffer; + } +} + +/* ------------------------------------------------------------------------- */ + +interface Predis_IResponseHandler { + function handle(Predis_Connection $connection, $payload); +} + +class Predis_ResponseStatusHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $status) { + if ($status === "OK") { + return true; + } + if ($status === "QUEUED") { + return new Predis_ResponseQueued(); + } + return $status; + } +} + +class Predis_ResponseErrorHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $errorMessage) { + throw new Predis_ServerException(substr($errorMessage, 4)); + } +} + +class Predis_ResponseErrorSilentHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $errorMessage) { + return new Predis_ResponseError(substr($errorMessage, 4)); + } +} + +class Predis_ResponseBulkHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $lengthString) { + $length = (int) $lengthString; + if ($length != $lengthString) { + Predis_Shared_Utils::onCommunicationException(new Predis_MalformedServerResponse( + $connection, "Cannot parse '$length' as data length" + )); + } + if ($length >= 0) { + return substr($connection->readBytes($length + 2), 0, -2); + } + if ($length == -1) { + return null; + } + } +} + +class Predis_ResponseMultiBulkHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $lengthString) { + $listLength = (int) $lengthString; + if ($listLength != $lengthString) { + Predis_Shared_Utils::onCommunicationException(new Predis_MalformedServerResponse( + $connection, "Cannot parse '$lengthString' as data length" + )); + } + + if ($listLength === -1) { + return null; + } + + $list = array(); + + if ($listLength > 0) { + $handlers = array(); + $reader = $connection->getResponseReader(); + for ($i = 0; $i < $listLength; $i++) { + $header = $connection->readLine(); + $prefix = $header[0]; + if (isset($handlers[$prefix])) { + $handler = $handlers[$prefix]; + } + else { + $handler = $reader->getHandler($prefix); + $handlers[$prefix] = $handler; + } + $list[$i] = $handler->handle($connection, substr($header, 1)); + } + } + + return $list; + } +} + +class Predis_ResponseMultiBulkStreamHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $lengthString) { + $listLength = (int) $lengthString; + if ($listLength != $lengthString) { + Predis_Shared_Utils::onCommunicationException(new Predis_MalformedServerResponse( + $connection, "Cannot parse '$lengthString' as data length" + )); + } + return new Predis_Shared_MultiBulkResponseIterator($connection, $lengthString); + } +} + +class Predis_ResponseIntegerHandler implements Predis_IResponseHandler { + public function handle(Predis_Connection $connection, $number) { + if (is_numeric($number)) { + return (int) $number; + } + else { + if ($number !== 'nil') { + Predis_Shared_Utils::onCommunicationException(new Predis_MalformedServerResponse( + $connection, "Cannot parse '$number' as numeric response" + )); + } + return null; + } + } +} + +interface Predis_IResponseReader { + public function read(Predis_Connection $connection); + public function setOption($option, $value); + public function getOption($option); +} + +class Predis_FastResponseReader implements Predis_IResponseReader { + private $_iterableMultibulk, $_throwErrors; + + public function __construct() { + $this->_iterableMultibulk = false; + $this->_throwErrors = true; + } + + public function read(Predis_Connection $connection) { + $chunk = $connection->readLine(); + $prefix = $chunk[0]; + $payload = substr($chunk, 1); + switch ($prefix) { + case '+': // inline + switch ($payload) { + case 'OK': + return true; + case 'QUEUED': + return new Predis_ResponseQueued(); + default: + return $payload; + } + + case '$': // bulk + $size = (int) $payload; + if ($size === -1) { + return null; + } + return substr($connection->readBytes($size + 2), 0, -2); + + case '*': // multi bulk + $count = (int) $payload; + if ($count === -1) { + return null; + } + if ($this->_iterableMultibulk) { + return new Predis_Shared_MultiBulkResponseIterator($connection, $count); + } + $multibulk = array(); + for ($i = 0; $i < $count; $i++) { + $multibulk[$i] = $this->read($connection); + } + return $multibulk; + + case ':': // integer + return (int) $payload; + + case '-': // error + $errorMessage = substr($payload, 4); + if ($this->_throwErrors) { + throw new Predis_ServerException($errorMessage); + } + return new Predis_ResponseError($errorMessage); + + default: + throw new Predis_CommunicationException( + $connection, "Unknown prefix: '$prefix'" + ); + } + } + + public function setOption($option, $value) { + switch ($option) { + case 'iterable_multibulk': + $this->_iterableMultibulk = (bool) $value; + break; + case 'throw_on_error': + $this->_throwErrors = (bool) $value; + break; + } + } + + public function getOption($option) { + switch ($option) { + case 'iterable_multibulk': + return $this->_iterableMultibulk; + case 'throw_on_error': + return $this->_throwErrors; + } + } +} + +class Predis_ResponseReader implements Predis_IResponseReader { + private $_prefixHandlers; + + public function __construct() { + $this->initializePrefixHandlers(); + } + + private function initializePrefixHandlers() { + $this->_prefixHandlers = array( + Predis_Protocol::PREFIX_STATUS => new Predis_ResponseStatusHandler(), + Predis_Protocol::PREFIX_ERROR => new Predis_ResponseErrorHandler(), + Predis_Protocol::PREFIX_INTEGER => new Predis_ResponseIntegerHandler(), + Predis_Protocol::PREFIX_BULK => new Predis_ResponseBulkHandler(), + Predis_Protocol::PREFIX_MULTI_BULK => new Predis_ResponseMultiBulkHandler(), + ); + } + + public function setHandler($prefix, Predis_IResponseHandler $handler) { + $this->_prefixHandlers[$prefix] = $handler; + } + + public function getHandler($prefix) { + if (isset($this->_prefixHandlers[$prefix])) { + return $this->_prefixHandlers[$prefix]; + } + } + + public function read(Predis_Connection $connection) { + $header = $connection->readLine(); + if ($header === '') { + $this->throwMalformedResponse($connection, 'Unexpected empty header'); + } + + $prefix = $header[0]; + if (!isset($this->_prefixHandlers[$prefix])) { + $this->throwMalformedResponse($connection, "Unknown prefix '$prefix'"); + } + + $handler = $this->_prefixHandlers[$prefix]; + return $handler->handle($connection, substr($header, 1)); + } + + private function throwMalformedResponse(Predis_Connection $connection, $message) { + Predis_Shared_Utils::onCommunicationException(new Predis_MalformedServerResponse( + $connection, $message + )); + } + + public function setOption($option, $value) { + switch ($option) { + case 'iterable_multibulk': + $handler = $value ? 'Predis_ResponseMultiBulkStreamHandler' : 'Predis_ResponseMultiBulkHandler'; + $this->_prefixHandlers[Predis_Protocol::PREFIX_MULTI_BULK] = new $handler(); + break; + case 'throw_on_error': + $handler = $value ? 'Predis_ResponseErrorHandler' : 'Predis_ResponseErrorSilentHandler'; + $this->_prefixHandlers[Predis_Protocol::PREFIX_ERROR] = new $handler(); + break; + } + } + + public function getOption($option) { + switch ($option) { + case 'iterable_multibulk': + return $this->_prefixHandlers[Predis_Protocol::PREFIX_MULTI_BULK] instanceof Predis_ResponseMultiBulkStreamHandler; + case 'throw_on_error': + return $this->_prefixHandlers[Predis_Protocol::PREFIX_ERROR] instanceof Predis_ResponseErrorHandler; + } + } +} + +class Predis_ResponseError { + public $skipParse = true; + private $_message; + + public function __construct($message) { + $this->_message = $message; + } + + public function __get($property) { + if ($property === 'error') { + return true; + } + if ($property === 'message') { + return $this->_message; + } + } + + public function __isset($property) { + return $property === 'error'; + } + + public function __toString() { + return $this->_message; + } +} + +class Predis_ResponseQueued { + public $skipParse = true; + + public function __toString() { + return Predis_Protocol::QUEUED; + } + + public function __get($property) { + if ($property === 'queued') { + return true; + } + } + + public function __isset($property) { + return $property === 'queued'; + } +} + +/* ------------------------------------------------------------------------- */ + +class Predis_CommandPipeline { + private $_redisClient, $_pipelineBuffer, $_returnValues, $_running, $_executor; + + public function __construct(Predis_Client $redisClient, Predis_Pipeline_IPipelineExecutor $executor = null) { + $this->_redisClient = $redisClient; + $this->_executor = $executor !== null ? $executor : new Predis_Pipeline_StandardExecutor(); + $this->_pipelineBuffer = array(); + $this->_returnValues = array(); + } + + public function __call($method, $arguments) { + $command = $this->_redisClient->createCommand($method, $arguments); + $this->recordCommand($command); + return $this; + } + + private function recordCommand(Predis_Command $command) { + $this->_pipelineBuffer[] = $command; + } + + private function getRecordedCommands() { + return $this->_pipelineBuffer; + } + + public function flushPipeline() { + if (count($this->_pipelineBuffer) > 0) { + $connection = $this->_redisClient->getConnection(); + $this->_returnValues = array_merge( + $this->_returnValues, + $this->_executor->execute($connection, $this->_pipelineBuffer) + ); + $this->_pipelineBuffer = array(); + } + return $this; + } + + private function setRunning($bool) { + if ($bool == true && $this->_running == true) { + throw new Predis_ClientException("This pipeline is already opened"); + } + $this->_running = $bool; + } + + public function execute($block = null) { + if ($block && !is_callable($block)) { + throw new InvalidArgumentException('Argument passed must be a callable object'); + } + + // TODO: do not reuse previously executed pipelines + $this->setRunning(true); + $pipelineBlockException = null; + + try { + if ($block !== null) { + $block($this); + } + $this->flushPipeline(); + } + catch (Exception $exception) { + $pipelineBlockException = $exception; + } + + $this->setRunning(false); + + if ($pipelineBlockException !== null) { + throw $pipelineBlockException; + } + + return $this->_returnValues; + } +} + +class Predis_MultiExecBlock { + private $_initialized, $_discarded, $_insideBlock, $_checkAndSet, $_watchedKeys; + private $_redisClient, $_options, $_commands; + private $_supportsWatch; + + public function __construct(Predis_Client $redisClient, Array $options = null) { + $this->checkCapabilities($redisClient); + $this->_options = isset($options) ? $options : array(); + $this->_redisClient = $redisClient; + $this->reset(); + } + + private function checkCapabilities(Predis_Client $redisClient) { + if (Predis_Shared_Utils::isCluster($redisClient->getConnection())) { + throw new Predis_ClientException( + 'Cannot initialize a MULTI/EXEC context over a cluster of connections' + ); + } + $profile = $redisClient->getProfile(); + if ($profile->supportsCommands(array('multi', 'exec', 'discard')) === false) { + throw new Predis_ClientException( + 'The current profile does not support MULTI, EXEC and DISCARD commands' + ); + } + $this->_supportsWatch = $profile->supportsCommands(array('watch', 'unwatch')); + } + + private function isWatchSupported() { + if ($this->_supportsWatch === false) { + throw new Predis_ClientException( + 'The current profile does not support WATCH and UNWATCH commands' + ); + } + } + + private function reset() { + $this->_initialized = false; + $this->_discarded = false; + $this->_checkAndSet = false; + $this->_insideBlock = false; + $this->_watchedKeys = false; + $this->_commands = array(); + } + + private function initialize() { + if ($this->_initialized === true) { + return; + } + $options = &$this->_options; + $this->_checkAndSet = isset($options['cas']) && $options['cas']; + if (isset($options['watch'])) { + $this->watch($options['watch']); + } + if (!$this->_checkAndSet || ($this->_discarded && $this->_checkAndSet)) { + $this->_redisClient->multi(); + if ($this->_discarded) { + $this->_checkAndSet = false; + } + } + $this->_initialized = true; + $this->_discarded = false; + } + + public function __call($method, $arguments) { + $this->initialize(); + $client = $this->_redisClient; + if ($this->_checkAndSet) { + return call_user_func_array(array($client, $method), $arguments); + } + $command = $client->createCommand($method, $arguments); + $response = $client->executeCommand($command); + if (!$response instanceof Predis_ResponseQueued) { + $this->malformedServerResponse( + 'The server did not respond with a QUEUED status reply' + ); + } + $this->_commands[] = $command; + return $this; + } + + public function watch($keys) { + $this->isWatchSupported(); + if ($this->_initialized && !$this->_checkAndSet) { + throw new Predis_ClientException('WATCH inside MULTI is not allowed'); + } + $this->_watchedKeys = true; + return $this->_redisClient->watch($keys); + } + + public function multi() { + if ($this->_initialized && $this->_checkAndSet) { + $this->_checkAndSet = false; + $this->_redisClient->multi(); + return $this; + } + $this->initialize(); + return $this; + } + + public function unwatch() { + $this->isWatchSupported(); + $this->_watchedKeys = false; + $this->_redisClient->unwatch(); + return $this; + } + + public function discard() { + if ($this->_initialized === true) { + $command = $this->_checkAndSet ? 'unwatch' : 'discard'; + $this->_redisClient->$command(); + $this->reset(); + $this->_discarded = true; + } + return $this; + } + + public function exec() { + return $this->execute(); + } + + private function checkBeforeExecution($block) { + if ($this->_insideBlock === true) { + throw new Predis_ClientException( + "Cannot invoke 'execute' or 'exec' inside an active client transaction block" + ); + } + if ($block) { + if (!is_callable($block)) { + throw new InvalidArgumentException( + 'Argument passed must be a callable object' + ); + } + if (count($this->_commands) > 0) { + $this->discard(); + throw new Predis_ClientException( + 'Cannot execute a transaction block after using fluent interface' + ); + } + } + if (isset($this->_options['retry']) && !isset($block)) { + $this->discard(); + throw new InvalidArgumentException( + 'Automatic retries can be used only when a transaction block is provided' + ); + } + } + + public function execute($block = null) { + $this->checkBeforeExecution($block); + + $reply = null; + $returnValues = array(); + $attemptsLeft = isset($this->_options['retry']) ? (int)$this->_options['retry'] : 0; + do { + $blockException = null; + if ($block !== null) { + $this->_insideBlock = true; + try { + $block($this); + } + catch (Predis_CommunicationException $exception) { + $blockException = $exception; + } + catch (Predis_ServerException $exception) { + $blockException = $exception; + } + catch (Exception $exception) { + $blockException = $exception; + $this->discard(); + } + $this->_insideBlock = false; + if ($blockException !== null) { + throw $blockException; + } + } + + if (count($this->_commands) === 0) { + if ($this->_watchedKeys) { + $this->discard(); + return; + } + return; + } + + $reply = $this->_redisClient->exec(); + if ($reply === null) { + if ($attemptsLeft === 0) { + throw new Predis_AbortedMultiExec( + 'The current transaction has been aborted by the server' + ); + } + $this->reset(); + if (isset($this->_options['on_retry']) && is_callable($this->_options['on_retry'])) { + call_user_func($this->_options['on_retry'], $this, $attemptsLeft); + } + continue; + } + break; + } while ($attemptsLeft-- > 0); + + $execReply = $reply instanceof Iterator ? iterator_to_array($reply) : $reply; + $sizeofReplies = count($execReply); + + $commands = &$this->_commands; + if ($sizeofReplies !== count($commands)) { + $this->malformedServerResponse( + 'Unexpected number of responses for a MultiExecBlock' + ); + } + for ($i = 0; $i < $sizeofReplies; $i++) { + $returnValues[] = $commands[$i]->parseResponse($execReply[$i] instanceof Iterator + ? iterator_to_array($execReply[$i]) + : $execReply[$i] + ); + unset($commands[$i]); + } + + return $returnValues; + } + + private function malformedServerResponse($message) { + // Since a MULTI/EXEC block cannot be initialized over a clustered + // connection, we can safely assume that Predis_Client::getConnection() + // will always return an instance of Predis_Connection. + Predis_Shared_Utils::onCommunicationException( + new Predis_MalformedServerResponse( + $this->_redisClient->getConnection(), $message + ) + ); + } +} + +class Predis_PubSubContext implements Iterator { + const SUBSCRIBE = 'subscribe'; + const UNSUBSCRIBE = 'unsubscribe'; + const PSUBSCRIBE = 'psubscribe'; + const PUNSUBSCRIBE = 'punsubscribe'; + const MESSAGE = 'message'; + const PMESSAGE = 'pmessage'; + + const STATUS_VALID = 0x0001; + const STATUS_SUBSCRIBED = 0x0010; + const STATUS_PSUBSCRIBED = 0x0100; + + private $_redisClient, $_position, $_options; + + public function __construct(Predis_Client $redisClient, Array $options = null) { + $this->checkCapabilities($redisClient); + $this->_options = isset($options) ? $options : array(); + $this->_redisClient = $redisClient; + $this->_statusFlags = self::STATUS_VALID; + + $this->genericSubscribeInit('subscribe'); + $this->genericSubscribeInit('psubscribe'); + } + + public function __destruct() { + if ($this->valid()) { + $this->closeContext(); + } + } + + private function checkCapabilities(Predis_Client $redisClient) { + if (Predis_Shared_Utils::isCluster($redisClient->getConnection())) { + throw new Predis_ClientException( + 'Cannot initialize a PUB/SUB context over a cluster of connections' + ); + } + $profile = $redisClient->getProfile(); + $commands = array('publish', 'subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe'); + if ($profile->supportsCommands($commands) === false) { + throw new Predis_ClientException( + 'The current profile does not support PUB/SUB related commands' + ); + } + } + + private function genericSubscribeInit($subscribeAction) { + if (isset($this->_options[$subscribeAction])) { + $this->$subscribeAction($this->_options[$subscribeAction]); + } + } + + private function isFlagSet($value) { + return ($this->_statusFlags & $value) === $value; + } + + public function subscribe(/* arguments */) { + $args = func_get_args(); + $this->writeCommand(self::SUBSCRIBE, $args); + $this->_statusFlags |= self::STATUS_SUBSCRIBED; + } + + public function unsubscribe(/* arguments */) { + $args = func_get_args(); + $this->writeCommand(self::UNSUBSCRIBE, $args); + } + + public function psubscribe(/* arguments */) { + $args = func_get_args(); + $this->writeCommand(self::PSUBSCRIBE, $args); + $this->_statusFlags |= self::STATUS_PSUBSCRIBED; + } + + public function punsubscribe(/* arguments */) { + $args = func_get_args(); + $this->writeCommand(self::PUNSUBSCRIBE, $args); + } + + public function closeContext() { + if ($this->valid()) { + if ($this->isFlagSet(self::STATUS_SUBSCRIBED)) { + $this->unsubscribe(); + } + if ($this->isFlagSet(self::STATUS_PSUBSCRIBED)) { + $this->punsubscribe(); + } + } + } + + private function writeCommand($method, $arguments) { + if (count($arguments) === 1 && is_array($arguments[0])) { + $arguments = $arguments[0]; + } + $command = $this->_redisClient->createCommand($method, $arguments); + $this->_redisClient->getConnection()->writeCommand($command); + } + + public function rewind() { + // NOOP + } + + public function current() { + return $this->getValue(); + } + + public function key() { + return $this->_position; + } + + public function next() { + if ($this->isFlagSet(self::STATUS_VALID)) { + $this->_position++; + } + return $this->_position; + } + + public function valid() { + $subscriptions = self::STATUS_SUBSCRIBED + self::STATUS_PSUBSCRIBED; + return $this->isFlagSet(self::STATUS_VALID) + && ($this->_statusFlags & $subscriptions) > 0; + } + + private function invalidate() { + $this->_statusFlags = 0x0000; + } + + private function getValue() { + $reader = $this->_redisClient->getResponseReader(); + $connection = $this->_redisClient->getConnection(); + $response = $reader->read($connection); + + switch ($response[0]) { + case self::SUBSCRIBE: + case self::UNSUBSCRIBE: + case self::PSUBSCRIBE: + case self::PUNSUBSCRIBE: + if ($response[2] === 0) { + $this->invalidate(); + } + case self::MESSAGE: + return (object) array( + 'kind' => $response[0], + 'channel' => $response[1], + 'payload' => $response[2], + ); + case self::PMESSAGE: + return (object) array( + 'kind' => $response[0], + 'pattern' => $response[1], + 'channel' => $response[2], + 'payload' => $response[3], + ); + default: + throw new Predis_ClientException( + "Received an unknown message type {$response[0]} inside of a pubsub context" + ); + } + } +} + +/* ------------------------------------------------------------------------- */ + +class Predis_ConnectionParameters { + const DEFAULT_SCHEME = 'redis'; + const DEFAULT_HOST = '127.0.0.1'; + const DEFAULT_PORT = 6379; + const DEFAULT_TIMEOUT = 5; + + private static $_defaultParameters = array( + 'scheme' => self::DEFAULT_SCHEME, + 'host' => self::DEFAULT_HOST, + 'port' => self::DEFAULT_PORT, + 'database' => null, + 'password' => null, + 'connection_async' => false, + 'connection_persistent' => false, + 'connection_timeout' => self::DEFAULT_TIMEOUT, + 'read_write_timeout' => null, + 'alias' => null, + 'weight' => null, + 'path' => null, + ); + + private $_parameters; + + public function __construct($parameters = null) { + $parameters = $parameters !== null ? $parameters : array(); + $extractor = is_array($parameters) ? 'filter' : 'parseURI'; + $this->_parameters = $this->$extractor($parameters); + } + + private function parseURI($uri) { + if (stripos($uri, 'unix') === 0) { + // Hack to support URIs for UNIX sockets with minimal effort. + $uri = str_ireplace('unix:///', 'unix://localhost/', $uri); + } + if (($parsed = @parse_url($uri)) === false || !isset($parsed['host'])) { + throw new Predis_ClientException("Invalid URI: $uri"); + } + if (isset($parsed['query'])) { + foreach (explode('&', $parsed['query']) as $kv) { + @list($k, $v) = explode('=', $kv); + $parsed[$k] = $v; + } + unset($parsed['query']); + } + return $this->filter($parsed); + } + + private function filter($parameters) { + return array_merge(self::$_defaultParameters, $parameters); + } + + public function __get($parameter) { + return $this->_parameters[$parameter]; + } + + public function __isset($parameter) { + return isset($this->_parameters[$parameter]); + } +} + +interface Predis_IConnection { + public function connect(); + public function disconnect(); + public function isConnected(); + public function writeCommand(Predis_Command $command); + public function readResponse(Predis_Command $command); + public function executeCommand(Predis_Command $command); +} + +class Predis_Connection implements Predis_IConnection { + private static $_allowedSchemes = array('redis', 'tcp', 'unix'); + private $_params, $_socket, $_initCmds, $_reader; + + public function __construct(Predis_ConnectionParameters $parameters, Predis_IResponseReader $reader = null) { + if (!in_array($parameters->scheme, self::$_allowedSchemes)) { + throw new InvalidArgumentException("Invalid scheme: {$parameters->scheme}"); + } + $this->_params = $parameters; + $this->_initCmds = array(); + $this->_reader = $reader !== null ? $reader : new Predis_FastResponseReader(); + } + + public function __destruct() { + if (!$this->_params->connection_persistent) { + $this->disconnect(); + } + } + + public function isConnected() { + return isset($this->_socket); + } + + public function connect() { + if ($this->isConnected()) { + throw new Predis_ClientException('Connection already estabilished'); + } + $initializer = "{$this->_params->scheme}StreamInitializer"; + $this->_socket = $this->$initializer($this->_params); + if (count($this->_initCmds) > 0){ + $this->sendInitializationCommands(); + } + } + + private function tcpStreamInitializer(Predis_ConnectionParameters $parameters) { + return $this->redisStreamInitializer($parameters); + } + + private function redisStreamInitializer(Predis_ConnectionParameters $parameters) { + $uri = sprintf('tcp://%s:%d/', $parameters->host, $parameters->port); + $connectFlags = STREAM_CLIENT_CONNECT; + if ($parameters->connection_async) { + $connectFlags |= STREAM_CLIENT_ASYNC_CONNECT; + } + if ($parameters->connection_persistent) { + $connectFlags |= STREAM_CLIENT_PERSISTENT; + } + $socket = @stream_socket_client( + $uri, $errno, $errstr, $parameters->connection_timeout, $connectFlags + ); + + if (!$socket) { + $this->onCommunicationException(trim($errstr), $errno); + } + + if (isset($parameters->read_write_timeout)) { + $rwtimeout = $parameters->read_write_timeout; + $rwtimeout = $rwtimeout > 0 ? $rwtimeout : -1; + $timeoutSeconds = floor($parameters->read_write_timeout); + $timeoutUSeconds = ($rwtimeout - $timeoutSeconds) * 1000000; + stream_set_timeout($socket, $rwtimeout, $timeoutUSeconds); + } + return $socket; + } + + private function unixStreamInitializer(Predis_ConnectionParameters $parameters) { + $uri = sprintf('unix:///%s', $parameters->path); + $connectFlags = STREAM_CLIENT_CONNECT; + if ($parameters->connection_persistent) { + $connectFlags |= STREAM_CLIENT_PERSISTENT; + } + $socket = @stream_socket_client( + $uri, $errno, $errstr, $parameters->connection_timeout, $connectFlags + ); + if (!$socket) { + $this->onCommunicationException(trim($errstr), $errno); + } + return $socket; + } + + public function disconnect() { + if ($this->isConnected()) { + fclose($this->_socket); + unset($this->_socket); + } + } + + public function pushInitCommand(Predis_Command $command){ + $this->_initCmds[] = $command; + } + + private function sendInitializationCommands() { + foreach ($this->_initCmds as $command) { + $this->writeCommand($command); + } + foreach ($this->_initCmds as $command) { + $this->readResponse($command); + } + } + + private function onCommunicationException($message, $code = null) { + Predis_Shared_Utils::onCommunicationException( + new Predis_CommunicationException($this, $message, $code) + ); + } + + public function writeCommand(Predis_Command $command) { + $this->writeBytes($command->invoke()); + } + + public function readResponse(Predis_Command $command) { + $response = $this->_reader->read($this); + return isset($response->skipParse) ? $response : $command->parseResponse($response); + } + + public function executeCommand(Predis_Command $command) { + $this->writeCommand($command); + if ($command->closesConnection()) { + return $this->disconnect(); + } + return $this->readResponse($command); + } + + public function rawCommand($rawCommandData, $closesConnection = false) { + $this->writeBytes($rawCommandData); + if ($closesConnection) { + $this->disconnect(); + return; + } + return $this->_reader->read($this); + } + + public function writeBytes($value) { + $socket = $this->getSocket(); + while (($length = strlen($value)) > 0) { + $written = fwrite($socket, $value); + if ($length === $written) { + return true; + } + if ($written === false || $written === 0) { + $this->onCommunicationException('Error while writing bytes to the server'); + } + $value = substr($value, $written); + } + return true; + } + + public function readBytes($length) { + if ($length <= 0) { + throw new InvalidArgumentException('Length parameter must be greater than 0'); + } + $socket = $this->getSocket(); + $value = ''; + do { + $chunk = fread($socket, $length); + if ($chunk === false || $chunk === '') { + $this->onCommunicationException('Error while reading bytes from the server'); + } + $value .= $chunk; + } + while (($length -= strlen($chunk)) > 0); + return $value; + } + + public function readLine() { + $socket = $this->getSocket(); + $value = ''; + do { + $chunk = fgets($socket); + if ($chunk === false || $chunk === '') { + $this->onCommunicationException('Error while reading line from the server'); + } + $value .= $chunk; + } + while (substr($value, -2) !== "\r\n"); + return substr($value, 0, -2); + } + + public function getSocket() { + if (isset($this->_socket)) { + return $this->_socket; + } + $this->connect(); + return $this->_socket; + } + + public function getResponseReader() { + return $this->_reader; + } + + public function getParameters() { + return $this->_params; + } + + public function __toString() { + return sprintf('%s:%d', $this->_params->host, $this->_params->port); + } +} + +class Predis_ConnectionCluster implements Predis_IConnection, IteratorAggregate { + private $_pool, $_distributor; + + public function __construct(Predis_Distribution_IDistributionStrategy $distributor = null) { + $this->_pool = array(); + $this->_distributor = $distributor !== null ? $distributor : new Predis_Distribution_HashRing(); + } + + public function isConnected() { + foreach ($this->_pool as $connection) { + if ($connection->isConnected()) { + return true; + } + } + return false; + } + + public function connect() { + foreach ($this->_pool as $connection) { + $connection->connect(); + } + } + + public function disconnect() { + foreach ($this->_pool as $connection) { + $connection->disconnect(); + } + } + + public function add(Predis_Connection $connection) { + $parameters = $connection->getParameters(); + if (isset($parameters->alias)) { + $this->_pool[$parameters->alias] = $connection; + } + else { + $this->_pool[] = $connection; + } + $this->_distributor->add($connection, $parameters->weight); + } + + public function getConnection(Predis_Command $command) { + if ($command->canBeHashed() === false) { + throw new Predis_ClientException( + sprintf("Cannot send '%s' commands to a cluster of connections", $command->getCommandId()) + ); + } + return $this->_distributor->get($command->getHash($this->_distributor)); + } + + public function getConnectionById($id = null) { + $alias = $id !== null ? $id : 0; + return isset($this->_pool[$alias]) ? $this->_pool[$alias] : null; + } + + public function getIterator() { + return new ArrayIterator($this->_pool); + } + + public function writeCommand(Predis_Command $command) { + $this->getConnection($command)->writeCommand($command); + } + + public function readResponse(Predis_Command $command) { + return $this->getConnection($command)->readResponse($command); + } + + public function executeCommand(Predis_Command $command) { + $connection = $this->getConnection($command); + $connection->writeCommand($command); + return $connection->readResponse($command); + } +} + +/* ------------------------------------------------------------------------- */ + +abstract class Predis_RedisServerProfile { + private static $_serverProfiles; + private $_registeredCommands; + + public function __construct() { + $this->_registeredCommands = $this->getSupportedCommands(); + } + + public abstract function getVersion(); + + protected abstract function getSupportedCommands(); + + public static function getDefault() { + return self::get('default'); + } + + public static function getDevelopment() { + return self::get('dev'); + } + + private static function predisServerProfiles() { + return array( + '1.2' => 'Predis_RedisServer_v1_2', + '2.0' => 'Predis_RedisServer_v2_0', + '2.2' => 'Predis_RedisServer_v2_2', + 'default' => 'Predis_RedisServer_v2_2', + 'dev' => 'Predis_RedisServer_vNext', + ); + } + + public static function registerProfile($profileClass, $aliases) { + if (!isset(self::$_serverProfiles)) { + self::$_serverProfiles = self::predisServerProfiles(); + } + + $profileReflection = new ReflectionClass($profileClass); + + if (!$profileReflection->isSubclassOf('Predis_RedisServerProfile')) { + throw new Predis_ClientException("Cannot register '$profileClass' as it is not a valid profile class"); + } + + if (is_array($aliases)) { + foreach ($aliases as $alias) { + self::$_serverProfiles[$alias] = $profileClass; + } + } + else { + self::$_serverProfiles[$aliases] = $profileClass; + } + } + + public static function get($version) { + if (!isset(self::$_serverProfiles)) { + self::$_serverProfiles = self::predisServerProfiles(); + } + if (!isset(self::$_serverProfiles[$version])) { + throw new Predis_ClientException("Unknown server profile: $version"); + } + $profile = self::$_serverProfiles[$version]; + return new $profile(); + } + + public function supportsCommands(Array $commands) { + foreach ($commands as $command) { + if ($this->supportsCommand($command) === false) { + return false; + } + } + return true; + } + + public function supportsCommand($command) { + return isset($this->_registeredCommands[$command]); + } + + public function createCommand($method, $arguments = array()) { + if (!isset($this->_registeredCommands[$method])) { + throw new Predis_ClientException("'$method' is not a registered Redis command"); + } + $commandClass = $this->_registeredCommands[$method]; + $command = new $commandClass(); + $command->setArgumentsArray($arguments); + return $command; + } + + public function registerCommands(Array $commands) { + foreach ($commands as $command => $aliases) { + $this->registerCommand($command, $aliases); + } + } + + public function registerCommand($command, $aliases) { + $commandReflection = new ReflectionClass($command); + + if (!$commandReflection->isSubclassOf('Predis_Command')) { + throw new ClientException("Cannot register '$command' as it is not a valid Redis command"); + } + + if (is_array($aliases)) { + foreach ($aliases as $alias) { + $this->_registeredCommands[$alias] = $command; + } + } + else { + $this->_registeredCommands[$aliases] = $command; + } + } + + public function __toString() { + return $this->getVersion(); + } +} + +class Predis_RedisServer_v1_2 extends Predis_RedisServerProfile { + public function getVersion() { return '1.2'; } + public function getSupportedCommands() { + return array( + /* ---------------- Redis 1.2 ---------------- */ + + /* miscellaneous commands */ + 'ping' => 'Predis_Commands_Ping', + 'echo' => 'Predis_Commands_DoEcho', + 'auth' => 'Predis_Commands_Auth', + + /* connection handling */ + 'quit' => 'Predis_Commands_Quit', + + /* commands operating on string values */ + 'set' => 'Predis_Commands_Set', + 'setnx' => 'Predis_Commands_SetPreserve', + 'mset' => 'Predis_Commands_SetMultiple', + 'msetnx' => 'Predis_Commands_SetMultiplePreserve', + 'get' => 'Predis_Commands_Get', + 'mget' => 'Predis_Commands_GetMultiple', + 'getset' => 'Predis_Commands_GetSet', + 'incr' => 'Predis_Commands_Increment', + 'incrby' => 'Predis_Commands_IncrementBy', + 'decr' => 'Predis_Commands_Decrement', + 'decrby' => 'Predis_Commands_DecrementBy', + 'exists' => 'Predis_Commands_Exists', + 'del' => 'Predis_Commands_Delete', + 'type' => 'Predis_Commands_Type', + + /* commands operating on the key space */ + 'keys' => 'Predis_Commands_Keys_v1_2', + 'randomkey' => 'Predis_Commands_RandomKey', + 'rename' => 'Predis_Commands_Rename', + 'renamenx' => 'Predis_Commands_RenamePreserve', + 'expire' => 'Predis_Commands_Expire', + 'expireat' => 'Predis_Commands_ExpireAt', + 'dbsize' => 'Predis_Commands_DatabaseSize', + 'ttl' => 'Predis_Commands_TimeToLive', + + /* commands operating on lists */ + 'rpush' => 'Predis_Commands_ListPushTail', + 'lpush' => 'Predis_Commands_ListPushHead', + 'llen' => 'Predis_Commands_ListLength', + 'lrange' => 'Predis_Commands_ListRange', + 'ltrim' => 'Predis_Commands_ListTrim', + 'lindex' => 'Predis_Commands_ListIndex', + 'lset' => 'Predis_Commands_ListSet', + 'lrem' => 'Predis_Commands_ListRemove', + 'lpop' => 'Predis_Commands_ListPopFirst', + 'rpop' => 'Predis_Commands_ListPopLast', + 'rpoplpush' => 'Predis_Commands_ListPopLastPushHead', + + /* commands operating on sets */ + 'sadd' => 'Predis_Commands_SetAdd', + 'srem' => 'Predis_Commands_SetRemove', + 'spop' => 'Predis_Commands_SetPop', + 'smove' => 'Predis_Commands_SetMove', + 'scard' => 'Predis_Commands_SetCardinality', + 'sismember' => 'Predis_Commands_SetIsMember', + 'sinter' => 'Predis_Commands_SetIntersection', + 'sinterstore' => 'Predis_Commands_SetIntersectionStore', + 'sunion' => 'Predis_Commands_SetUnion', + 'sunionstore' => 'Predis_Commands_SetUnionStore', + 'sdiff' => 'Predis_Commands_SetDifference', + 'sdiffstore' => 'Predis_Commands_SetDifferenceStore', + 'smembers' => 'Predis_Commands_SetMembers', + 'srandmember' => 'Predis_Commands_SetRandomMember', + + /* commands operating on sorted sets */ + 'zadd' => 'Predis_Commands_ZSetAdd', + 'zincrby' => 'Predis_Commands_ZSetIncrementBy', + 'zrem' => 'Predis_Commands_ZSetRemove', + 'zrange' => 'Predis_Commands_ZSetRange', + 'zrevrange' => 'Predis_Commands_ZSetReverseRange', + 'zrangebyscore' => 'Predis_Commands_ZSetRangeByScore', + 'zcard' => 'Predis_Commands_ZSetCardinality', + 'zscore' => 'Predis_Commands_ZSetScore', + 'zremrangebyscore' => 'Predis_Commands_ZSetRemoveRangeByScore', + + /* multiple databases handling commands */ + 'select' => 'Predis_Commands_SelectDatabase', + 'move' => 'Predis_Commands_MoveKey', + 'flushdb' => 'Predis_Commands_FlushDatabase', + 'flushall' => 'Predis_Commands_FlushAll', + + /* sorting */ + 'sort' => 'Predis_Commands_Sort', + + /* remote server control commands */ + 'info' => 'Predis_Commands_Info', + 'slaveof' => 'Predis_Commands_SlaveOf', + + /* persistence control commands */ + 'save' => 'Predis_Commands_Save', + 'bgsave' => 'Predis_Commands_BackgroundSave', + 'lastsave' => 'Predis_Commands_LastSave', + 'shutdown' => 'Predis_Commands_Shutdown', + 'bgrewriteaof' => 'Predis_Commands_BackgroundRewriteAppendOnlyFile', + ); + } +} + +class Predis_RedisServer_v2_0 extends Predis_RedisServerProfile { + public function getVersion() { return '2.0'; } + public function getSupportedCommands() { + return array( + /* ---------------- Redis 1.2 ---------------- */ + + /* miscellaneous commands */ + 'ping' => 'Predis_Commands_Ping', + 'echo' => 'Predis_Commands_DoEcho', + 'auth' => 'Predis_Commands_Auth', + + /* connection handling */ + 'quit' => 'Predis_Commands_Quit', + + /* commands operating on string values */ + 'set' => 'Predis_Commands_Set', + 'setnx' => 'Predis_Commands_SetPreserve', + 'mset' => 'Predis_Commands_SetMultiple', + 'msetnx' => 'Predis_Commands_SetMultiplePreserve', + 'get' => 'Predis_Commands_Get', + 'mget' => 'Predis_Commands_GetMultiple', + 'getset' => 'Predis_Commands_GetSet', + 'incr' => 'Predis_Commands_Increment', + 'incrby' => 'Predis_Commands_IncrementBy', + 'decr' => 'Predis_Commands_Decrement', + 'decrby' => 'Predis_Commands_DecrementBy', + 'exists' => 'Predis_Commands_Exists', + 'del' => 'Predis_Commands_Delete', + 'type' => 'Predis_Commands_Type', + + /* commands operating on the key space */ + 'keys' => 'Predis_Commands_Keys', + 'randomkey' => 'Predis_Commands_RandomKey', + 'rename' => 'Predis_Commands_Rename', + 'renamenx' => 'Predis_Commands_RenamePreserve', + 'expire' => 'Predis_Commands_Expire', + 'expireat' => 'Predis_Commands_ExpireAt', + 'dbsize' => 'Predis_Commands_DatabaseSize', + 'ttl' => 'Predis_Commands_TimeToLive', + + /* commands operating on lists */ + 'rpush' => 'Predis_Commands_ListPushTail', + 'lpush' => 'Predis_Commands_ListPushHead', + 'llen' => 'Predis_Commands_ListLength', + 'lrange' => 'Predis_Commands_ListRange', + 'ltrim' => 'Predis_Commands_ListTrim', + 'lindex' => 'Predis_Commands_ListIndex', + 'lset' => 'Predis_Commands_ListSet', + 'lrem' => 'Predis_Commands_ListRemove', + 'lpop' => 'Predis_Commands_ListPopFirst', + 'rpop' => 'Predis_Commands_ListPopLast', + 'rpoplpush' => 'Predis_Commands_ListPopLastPushHead', + + /* commands operating on sets */ + 'sadd' => 'Predis_Commands_SetAdd', + 'srem' => 'Predis_Commands_SetRemove', + 'spop' => 'Predis_Commands_SetPop', + 'smove' => 'Predis_Commands_SetMove', + 'scard' => 'Predis_Commands_SetCardinality', + 'sismember' => 'Predis_Commands_SetIsMember', + 'sinter' => 'Predis_Commands_SetIntersection', + 'sinterstore' => 'Predis_Commands_SetIntersectionStore', + 'sunion' => 'Predis_Commands_SetUnion', + 'sunionstore' => 'Predis_Commands_SetUnionStore', + 'sdiff' => 'Predis_Commands_SetDifference', + 'sdiffstore' => 'Predis_Commands_SetDifferenceStore', + 'smembers' => 'Predis_Commands_SetMembers', + 'srandmember' => 'Predis_Commands_SetRandomMember', + + /* commands operating on sorted sets */ + 'zadd' => 'Predis_Commands_ZSetAdd', + 'zincrby' => 'Predis_Commands_ZSetIncrementBy', + 'zrem' => 'Predis_Commands_ZSetRemove', + 'zrange' => 'Predis_Commands_ZSetRange', + 'zrevrange' => 'Predis_Commands_ZSetReverseRange', + 'zrangebyscore' => 'Predis_Commands_ZSetRangeByScore', + 'zcard' => 'Predis_Commands_ZSetCardinality', + 'zscore' => 'Predis_Commands_ZSetScore', + 'zremrangebyscore' => 'Predis_Commands_ZSetRemoveRangeByScore', + + /* multiple databases handling commands */ + 'select' => 'Predis_Commands_SelectDatabase', + 'move' => 'Predis_Commands_MoveKey', + 'flushdb' => 'Predis_Commands_FlushDatabase', + 'flushall' => 'Predis_Commands_FlushAll', + + /* sorting */ + 'sort' => 'Predis_Commands_Sort', + + /* remote server control commands */ + 'info' => 'Predis_Commands_Info', + 'slaveof' => 'Predis_Commands_SlaveOf', + + /* persistence control commands */ + 'save' => 'Predis_Commands_Save', + 'bgsave' => 'Predis_Commands_BackgroundSave', + 'lastsave' => 'Predis_Commands_LastSave', + 'shutdown' => 'Predis_Commands_Shutdown', + 'bgrewriteaof' => 'Predis_Commands_BackgroundRewriteAppendOnlyFile', + + + /* ---------------- Redis 2.0 ---------------- */ + + /* transactions */ + 'multi' => 'Predis_Commands_Multi', + 'exec' => 'Predis_Commands_Exec', + 'discard' => 'Predis_Commands_Discard', + + /* commands operating on string values */ + 'setex' => 'Predis_Commands_SetExpire', + 'append' => 'Predis_Commands_Append', + 'substr' => 'Predis_Commands_Substr', + + /* commands operating on lists */ + 'blpop' => 'Predis_Commands_ListPopFirstBlocking', + 'brpop' => 'Predis_Commands_ListPopLastBlocking', + + /* commands operating on sorted sets */ + 'zunionstore' => 'Predis_Commands_ZSetUnionStore', + 'zinterstore' => 'Predis_Commands_ZSetIntersectionStore', + 'zcount' => 'Predis_Commands_ZSetCount', + 'zrank' => 'Predis_Commands_ZSetRank', + 'zrevrank' => 'Predis_Commands_ZSetReverseRank', + 'zremrangebyrank' => 'Predis_Commands_ZSetRemoveRangeByRank', + + /* commands operating on hashes */ + 'hset' => 'Predis_Commands_HashSet', + 'hsetnx' => 'Predis_Commands_HashSetPreserve', + 'hmset' => 'Predis_Commands_HashSetMultiple', + 'hincrby' => 'Predis_Commands_HashIncrementBy', + 'hget' => 'Predis_Commands_HashGet', + 'hmget' => 'Predis_Commands_HashGetMultiple', + 'hdel' => 'Predis_Commands_HashDelete', + 'hexists' => 'Predis_Commands_HashExists', + 'hlen' => 'Predis_Commands_HashLength', + 'hkeys' => 'Predis_Commands_HashKeys', + 'hvals' => 'Predis_Commands_HashValues', + 'hgetall' => 'Predis_Commands_HashGetAll', + + /* publish - subscribe */ + 'subscribe' => 'Predis_Commands_Subscribe', + 'unsubscribe' => 'Predis_Commands_Unsubscribe', + 'psubscribe' => 'Predis_Commands_SubscribeByPattern', + 'punsubscribe' => 'Predis_Commands_UnsubscribeByPattern', + 'publish' => 'Predis_Commands_Publish', + + /* remote server control commands */ + 'config' => 'Predis_Commands_Config', + ); + } +} + +class Predis_RedisServer_v2_2 extends Predis_RedisServerProfile { + public function getVersion() { return '2.2'; } + public function getSupportedCommands() { + return array( + /* ---------------- Redis 1.2 ---------------- */ + + /* miscellaneous commands */ + 'ping' => 'Predis_Commands_Ping', + 'echo' => 'Predis_Commands_DoEcho', + 'auth' => 'Predis_Commands_Auth', + + /* connection handling */ + 'quit' => 'Predis_Commands_Quit', + + /* commands operating on string values */ + 'set' => 'Predis_Commands_Set', + 'setnx' => 'Predis_Commands_SetPreserve', + 'mset' => 'Predis_Commands_SetMultiple', + 'msetnx' => 'Predis_Commands_SetMultiplePreserve', + 'get' => 'Predis_Commands_Get', + 'mget' => 'Predis_Commands_GetMultiple', + 'getset' => 'Predis_Commands_GetSet', + 'incr' => 'Predis_Commands_Increment', + 'incrby' => 'Predis_Commands_IncrementBy', + 'decr' => 'Predis_Commands_Decrement', + 'decrby' => 'Predis_Commands_DecrementBy', + 'exists' => 'Predis_Commands_Exists', + 'del' => 'Predis_Commands_Delete', + 'type' => 'Predis_Commands_Type', + + /* commands operating on the key space */ + 'keys' => 'Predis_Commands_Keys', + 'randomkey' => 'Predis_Commands_RandomKey', + 'rename' => 'Predis_Commands_Rename', + 'renamenx' => 'Predis_Commands_RenamePreserve', + 'expire' => 'Predis_Commands_Expire', + 'expireat' => 'Predis_Commands_ExpireAt', + 'dbsize' => 'Predis_Commands_DatabaseSize', + 'ttl' => 'Predis_Commands_TimeToLive', + + /* commands operating on lists */ + 'rpush' => 'Predis_Commands_ListPushTail', + 'lpush' => 'Predis_Commands_ListPushHead', + 'llen' => 'Predis_Commands_ListLength', + 'lrange' => 'Predis_Commands_ListRange', + 'ltrim' => 'Predis_Commands_ListTrim', + 'lindex' => 'Predis_Commands_ListIndex', + 'lset' => 'Predis_Commands_ListSet', + 'lrem' => 'Predis_Commands_ListRemove', + 'lpop' => 'Predis_Commands_ListPopFirst', + 'rpop' => 'Predis_Commands_ListPopLast', + 'rpoplpush' => 'Predis_Commands_ListPopLastPushHead', + + /* commands operating on sets */ + 'sadd' => 'Predis_Commands_SetAdd', + 'srem' => 'Predis_Commands_SetRemove', + 'spop' => 'Predis_Commands_SetPop', + 'smove' => 'Predis_Commands_SetMove', + 'scard' => 'Predis_Commands_SetCardinality', + 'sismember' => 'Predis_Commands_SetIsMember', + 'sinter' => 'Predis_Commands_SetIntersection', + 'sinterstore' => 'Predis_Commands_SetIntersectionStore', + 'sunion' => 'Predis_Commands_SetUnion', + 'sunionstore' => 'Predis_Commands_SetUnionStore', + 'sdiff' => 'Predis_Commands_SetDifference', + 'sdiffstore' => 'Predis_Commands_SetDifferenceStore', + 'smembers' => 'Predis_Commands_SetMembers', + 'srandmember' => 'Predis_Commands_SetRandomMember', + + /* commands operating on sorted sets */ + 'zadd' => 'Predis_Commands_ZSetAdd', + 'zincrby' => 'Predis_Commands_ZSetIncrementBy', + 'zrem' => 'Predis_Commands_ZSetRemove', + 'zrange' => 'Predis_Commands_ZSetRange', + 'zrevrange' => 'Predis_Commands_ZSetReverseRange', + 'zrangebyscore' => 'Predis_Commands_ZSetRangeByScore', + 'zcard' => 'Predis_Commands_ZSetCardinality', + 'zscore' => 'Predis_Commands_ZSetScore', + 'zremrangebyscore' => 'Predis_Commands_ZSetRemoveRangeByScore', + + /* multiple databases handling commands */ + 'select' => 'Predis_Commands_SelectDatabase', + 'move' => 'Predis_Commands_MoveKey', + 'flushdb' => 'Predis_Commands_FlushDatabase', + 'flushall' => 'Predis_Commands_FlushAll', + + /* sorting */ + 'sort' => 'Predis_Commands_Sort', + + /* remote server control commands */ + 'info' => 'Predis_Commands_Info', + 'slaveof' => 'Predis_Commands_SlaveOf', + + /* persistence control commands */ + 'save' => 'Predis_Commands_Save', + 'bgsave' => 'Predis_Commands_BackgroundSave', + 'lastsave' => 'Predis_Commands_LastSave', + 'shutdown' => 'Predis_Commands_Shutdown', + 'bgrewriteaof' => 'Predis_Commands_BackgroundRewriteAppendOnlyFile', + + + /* ---------------- Redis 2.0 ---------------- */ + + /* transactions */ + 'multi' => 'Predis_Commands_Multi', + 'exec' => 'Predis_Commands_Exec', + 'discard' => 'Predis_Commands_Discard', + + /* commands operating on string values */ + 'setex' => 'Predis_Commands_SetExpire', + 'append' => 'Predis_Commands_Append', + 'substr' => 'Predis_Commands_Substr', + + /* commands operating on lists */ + 'blpop' => 'Predis_Commands_ListPopFirstBlocking', + 'brpop' => 'Predis_Commands_ListPopLastBlocking', + + /* commands operating on sorted sets */ + 'zunionstore' => 'Predis_Commands_ZSetUnionStore', + 'zinterstore' => 'Predis_Commands_ZSetIntersectionStore', + 'zcount' => 'Predis_Commands_ZSetCount', + 'zrank' => 'Predis_Commands_ZSetRank', + 'zrevrank' => 'Predis_Commands_ZSetReverseRank', + 'zremrangebyrank' => 'Predis_Commands_ZSetRemoveRangeByRank', + + /* commands operating on hashes */ + 'hset' => 'Predis_Commands_HashSet', + 'hsetnx' => 'Predis_Commands_HashSetPreserve', + 'hmset' => 'Predis_Commands_HashSetMultiple', + 'hincrby' => 'Predis_Commands_HashIncrementBy', + 'hget' => 'Predis_Commands_HashGet', + 'hmget' => 'Predis_Commands_HashGetMultiple', + 'hdel' => 'Predis_Commands_HashDelete', + 'hexists' => 'Predis_Commands_HashExists', + 'hlen' => 'Predis_Commands_HashLength', + 'hkeys' => 'Predis_Commands_HashKeys', + 'hvals' => 'Predis_Commands_HashValues', + 'hgetall' => 'Predis_Commands_HashGetAll', + + /* publish - subscribe */ + 'subscribe' => 'Predis_Commands_Subscribe', + 'unsubscribe' => 'Predis_Commands_Unsubscribe', + 'psubscribe' => 'Predis_Commands_SubscribeByPattern', + 'punsubscribe' => 'Predis_Commands_UnsubscribeByPattern', + 'publish' => 'Predis_Commands_Publish', + + /* remote server control commands */ + 'config' => 'Predis_Commands_Config', + + + /* ---------------- Redis 2.2 ---------------- */ + + /* transactions */ + 'watch' => 'Predis_Commands_Watch', + 'unwatch' => 'Predis_Commands_Unwatch', + + /* commands operating on string values */ + 'strlen' => 'Predis_Commands_Strlen', + 'setrange' => 'Predis_Commands_SetRange', + 'getrange' => 'Predis_Commands_Substr', + 'setbit' => 'Predis_Commands_SetBit', + 'getbit' => 'Predis_Commands_GetBit', + + /* commands operating on the key space */ + 'persist' => 'Predis_Commands_Persist', + + /* commands operating on lists */ + 'rpushx' => 'Predis_Commands_ListPushTailX', + 'lpushx' => 'Predis_Commands_ListPushHeadX', + 'linsert' => 'Predis_Commands_ListInsert', + 'brpoplpush' => 'Predis_Commands_ListPopLastPushHeadBlocking', + + /* commands operating on sorted sets */ + 'zrevrangebyscore' => 'Predis_Commands_ZSetReverseRangeByScore', + ); + } +} + +class Predis_RedisServer_vNext extends Predis_RedisServer_v2_2 { + public function getVersion() { return 'DEV'; } + public function getSupportedCommands() { + return array_merge(parent::getSupportedCommands(), array( + /* remote server control commands */ + 'info' => 'Predis_Commands_Info_v24', + )); + } +} + +/* ------------------------------------------------------------------------- */ + +interface Predis_Pipeline_IPipelineExecutor { + public function execute(Predis_IConnection $connection, &$commands); +} + +class Predis_Pipeline_StandardExecutor implements Predis_Pipeline_IPipelineExecutor { + public function execute(Predis_IConnection $connection, &$commands) { + $sizeofPipe = count($commands); + $values = array(); + + foreach ($commands as $command) { + $connection->writeCommand($command); + } + try { + for ($i = 0; $i < $sizeofPipe; $i++) { + $response = $connection->readResponse($commands[$i]); + $values[] = $response instanceof Iterator + ? iterator_to_array($response) + : $response; + unset($commands[$i]); + } + } + catch (Predis_ServerException $exception) { + // force disconnection to prevent protocol desynchronization + $connection->disconnect(); + throw $exception; + } + + return $values; + } +} + +class Predis_Pipeline_SafeExecutor implements Predis_Pipeline_IPipelineExecutor { + public function execute(Predis_IConnection $connection, &$commands) { + $sizeofPipe = count($commands); + $values = array(); + + foreach ($commands as $command) { + try { + $connection->writeCommand($command); + } + catch (Predis_CommunicationException $exception) { + return array_fill(0, $sizeofPipe, $exception); + } + } + + for ($i = 0; $i < $sizeofPipe; $i++) { + $command = $commands[$i]; + unset($commands[$i]); + try { + $response = $connection->readResponse($command); + $values[] = ($response instanceof Iterator + ? iterator_to_array($response) + : $response + ); + } + catch (Predis_ServerException $exception) { + $values[] = $exception->toResponseError(); + } + catch (Predis_CommunicationException $exception) { + $toAdd = count($commands) - count($values); + $values = array_merge($values, array_fill(0, $toAdd, $exception)); + break; + } + } + + return $values; + } +} + +class Predis_Pipeline_SafeClusterExecutor implements Predis_Pipeline_IPipelineExecutor { + public function execute(Predis_IConnection $connection, &$commands) { + $connectionExceptions = array(); + $sizeofPipe = count($commands); + $values = array(); + + foreach ($commands as $command) { + $cmdConnection = $connection->getConnection($command); + if (isset($connectionExceptions[spl_object_hash($cmdConnection)])) { + continue; + } + try { + $cmdConnection->writeCommand($command); + } + catch (Predis_CommunicationException $exception) { + $connectionExceptions[spl_object_hash($cmdConnection)] = $exception; + } + } + + for ($i = 0; $i < $sizeofPipe; $i++) { + $command = $commands[$i]; + unset($commands[$i]); + + $cmdConnection = $connection->getConnection($command); + $connectionObjectHash = spl_object_hash($cmdConnection); + + if (isset($connectionExceptions[$connectionObjectHash])) { + $values[] = $connectionExceptions[$connectionObjectHash]; + continue; + } + + try { + $response = $cmdConnection->readResponse($command); + $values[] = ($response instanceof Iterator + ? iterator_to_array($response) + : $response + ); + } + catch (Predis_ServerException $exception) { + $values[] = $exception->toResponseError(); + } + catch (Predis_CommunicationException $exception) { + $values[] = $exception; + $connectionExceptions[$connectionObjectHash] = $exception; + } + } + + return $values; + } +} + +/* ------------------------------------------------------------------------- */ + +interface Predis_Distribution_IDistributionStrategy { + public function add($node, $weight = null); + public function remove($node); + public function get($key); + public function generateKey($value); +} + +class Predis_Distribution_EmptyRingException extends Exception { } + +class Predis_Distribution_HashRing implements Predis_Distribution_IDistributionStrategy { + const DEFAULT_REPLICAS = 128; + const DEFAULT_WEIGHT = 100; + private $_nodes, $_ring, $_ringKeys, $_ringKeysCount, $_replicas; + + public function __construct($replicas = self::DEFAULT_REPLICAS) { + $this->_replicas = $replicas; + $this->_nodes = array(); + } + + public function add($node, $weight = null) { + // NOTE: in case of collisions in the hashes of the nodes, the node added + // last wins, thus the order in which nodes are added is significant. + // TODO: self::DEFAULT_WEIGHT does not work for inherited classes that + // override the DEFAULT_WEIGHT constant. + $this->_nodes[] = array( + 'object' => $node, + 'weight' => (int) ($weight !== null ? $weight : self::DEFAULT_WEIGHT), + ); + $this->reset(); + } + + public function remove($node) { + // NOTE: a node is removed by resetting the ring so that it's recreated from + // scratch, in order to reassign possible hashes with collisions to the + // right node according to the order in which they were added in the + // first place. + for ($i = 0; $i < count($this->_nodes); ++$i) { + if ($this->_nodes[$i]['object'] === $node) { + array_splice($this->_nodes, $i, 1); + $this->reset(); + break; + } + } + } + + private function reset() { + unset($this->_ring); + unset($this->_ringKeys); + unset($this->_ringKeysCount); + } + + private function isInitialized() { + return isset($this->_ringKeys); + } + + private function computeTotalWeight() { + $totalWeight = 0; + foreach ($this->_nodes as $node) { + $totalWeight += $node['weight']; + } + return $totalWeight; + } + + private function initialize() { + if ($this->isInitialized()) { + return; + } + if (count($this->_nodes) === 0) { + throw new Predis_Distribution_EmptyRingException('Cannot initialize empty hashring'); + } + + $this->_ring = array(); + $totalWeight = $this->computeTotalWeight(); + $nodesCount = count($this->_nodes); + foreach ($this->_nodes as $node) { + $weightRatio = $node['weight'] / $totalWeight; + $this->addNodeToRing($this->_ring, $node, $nodesCount, $this->_replicas, $weightRatio); + } + ksort($this->_ring, SORT_NUMERIC); + $this->_ringKeys = array_keys($this->_ring); + $this->_ringKeysCount = count($this->_ringKeys); + } + + protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { + $nodeObject = $node['object']; + $nodeHash = (string) $nodeObject; + $replicas = (int) round($weightRatio * $totalNodes * $replicas); + for ($i = 0; $i < $replicas; $i++) { + $key = crc32("$nodeHash:$i"); + $ring[$key] = $nodeObject; + } + } + + public function generateKey($value) { + return crc32($value); + } + + public function get($key) { + return $this->_ring[$this->getNodeKey($key)]; + } + + private function getNodeKey($key) { + $this->initialize(); + $ringKeys = $this->_ringKeys; + $upper = $this->_ringKeysCount - 1; + $lower = 0; + + while ($lower <= $upper) { + $index = ($lower + $upper) >> 1; + $item = $ringKeys[$index]; + if ($item > $key) { + $upper = $index - 1; + } + else if ($item < $key) { + $lower = $index + 1; + } + else { + return $item; + } + } + return $ringKeys[$this->wrapAroundStrategy($upper, $lower, $this->_ringKeysCount)]; + } + + protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { + // NOTE: binary search for the last item in _ringkeys with a value + // less or equal to the key. If no such item exists, return the + // last item. + return $upper >= 0 ? $upper : $ringKeysCount - 1; + } +} + +class Predis_Distribution_KetamaPureRing extends Predis_Distribution_HashRing { + const DEFAULT_REPLICAS = 160; + + public function __construct() { + parent::__construct(self::DEFAULT_REPLICAS); + } + + protected function addNodeToRing(&$ring, $node, $totalNodes, $replicas, $weightRatio) { + $nodeObject = $node['object']; + $nodeHash = (string) $nodeObject; + $replicas = (int) floor($weightRatio * $totalNodes * ($replicas / 4)); + for ($i = 0; $i < $replicas; $i++) { + $unpackedDigest = unpack('V4', md5("$nodeHash-$i", true)); + foreach ($unpackedDigest as $key) { + $ring[$key] = $nodeObject; + } + } + } + + public function generateKey($value) { + $hash = unpack('V', md5($value, true)); + return $hash[1]; + } + + protected function wrapAroundStrategy($upper, $lower, $ringKeysCount) { + // NOTE: binary search for the first item in _ringkeys with a value + // greater or equal to the key. If no such item exists, return the + // first item. + return $lower < $ringKeysCount ? $lower : 0; + } +} + +/* ------------------------------------------------------------------------- */ + +class Predis_Shared_Utils { + public static function isCluster(Predis_IConnection $connection) { + return $connection instanceof Predis_ConnectionCluster; + } + + public static function onCommunicationException(Predis_CommunicationException $exception) { + if ($exception->shouldResetConnection()) { + $connection = $exception->getConnection(); + if ($connection->isConnected()) { + $connection->disconnect(); + } + } + throw $exception; + } + + public static function filterArrayArguments(Array $arguments) { + if (count($arguments) === 1 && is_array($arguments[0])) { + return $arguments[0]; + } + return $arguments; + } +} + +abstract class Predis_Shared_MultiBulkResponseIteratorBase implements Iterator, Countable { + protected $_position, $_current, $_replySize; + + public function rewind() { + // NOOP + } + + public function current() { + return $this->_current; + } + + public function key() { + return $this->_position; + } + + public function next() { + if (++$this->_position < $this->_replySize) { + $this->_current = $this->getValue(); + } + return $this->_position; + } + + public function valid() { + return $this->_position < $this->_replySize; + } + + public function count() { + // NOTE: use count if you want to get the size of the current multi-bulk + // response without using iterator_count (which actually consumes + // our iterator to calculate the size, and we cannot perform a rewind) + return $this->_replySize; + } + + protected abstract function getValue(); +} + +class Predis_Shared_MultiBulkResponseIterator extends Predis_Shared_MultiBulkResponseIteratorBase { + private $_connection; + + public function __construct(Predis_Connection $connection, $size) { + $this->_connection = $connection; + $this->_reader = $connection->getResponseReader(); + $this->_position = 0; + $this->_current = $size > 0 ? $this->getValue() : null; + $this->_replySize = $size; + } + + public function __destruct() { + // when the iterator is garbage-collected (e.g. it goes out of the + // scope of a foreach) but it has not reached its end, we must sync + // the client with the queued elements that have not been read from + // the connection with the server. + $this->sync(); + } + + public function sync($drop = false) { + if ($drop == true) { + if ($this->valid()) { + $this->_position = $this->_replySize; + $this->_connection->disconnect(); + } + } + else { + while ($this->valid()) { + $this->next(); + } + } + } + + protected function getValue() { + return $this->_reader->read($this->_connection); + } +} + +class Predis_Shared_MultiBulkResponseKVIterator extends Predis_Shared_MultiBulkResponseIteratorBase { + private $_iterator; + + public function __construct(Predis_Shared_MultiBulkResponseIterator $iterator) { + $virtualSize = count($iterator) / 2; + + $this->_iterator = $iterator; + $this->_position = 0; + $this->_current = $virtualSize > 0 ? $this->getValue() : null; + $this->_replySize = $virtualSize; + } + + public function __destruct() { + $this->_iterator->sync(); + } + + protected function getValue() { + $k = $this->_iterator->current(); + $this->_iterator->next(); + $v = $this->_iterator->current(); + $this->_iterator->next(); + return array($k, $v); + } +} + +/* ------------------------------------------------------------------------- */ + +/* miscellaneous commands */ +class Predis_Commands_Ping extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'PING'; } + public function parseResponse($data) { + return $data === 'PONG'; + } +} + +class Predis_Commands_DoEcho extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'ECHO'; } +} + +class Predis_Commands_Auth extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'AUTH'; } +} + +/* connection handling */ +class Predis_Commands_Quit extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'QUIT'; } + public function closesConnection() { return true; } +} + +/* commands operating on string values */ +class Predis_Commands_Set extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SET'; } +} + +class Predis_Commands_SetExpire extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SETEX'; } +} + +class Predis_Commands_SetPreserve extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SETNX'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_SetMultiple extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'MSET'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 1 && is_array($arguments[0])) { + $flattenedKVs = array(); + $args = &$arguments[0]; + foreach ($args as $k => $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + return $flattenedKVs; + } + return $arguments; + } +} + +class Predis_Commands_SetMultiplePreserve extends Predis_Commands_SetMultiple { + public function canBeHashed() { return false; } + public function getCommandId() { return 'MSETNX'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_Get extends Predis_MultiBulkCommand { + public function getCommandId() { return 'GET'; } +} + +class Predis_Commands_GetMultiple extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'MGET'; } + public function filterArguments(Array $arguments) { + return Predis_Shared_Utils::filterArrayArguments($arguments); + } +} + +class Predis_Commands_GetSet extends Predis_MultiBulkCommand { + public function getCommandId() { return 'GETSET'; } +} + +class Predis_Commands_Increment extends Predis_MultiBulkCommand { + public function getCommandId() { return 'INCR'; } +} + +class Predis_Commands_IncrementBy extends Predis_MultiBulkCommand { + public function getCommandId() { return 'INCRBY'; } +} + +class Predis_Commands_Decrement extends Predis_MultiBulkCommand { + public function getCommandId() { return 'DECR'; } +} + +class Predis_Commands_DecrementBy extends Predis_MultiBulkCommand { + public function getCommandId() { return 'DECRBY'; } +} + +class Predis_Commands_Exists extends Predis_MultiBulkCommand { + public function getCommandId() { return 'EXISTS'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_Delete extends Predis_MultiBulkCommand { + public function getCommandId() { return 'DEL'; } +} + +class Predis_Commands_Type extends Predis_MultiBulkCommand { + public function getCommandId() { return 'TYPE'; } +} + +class Predis_Commands_Append extends Predis_MultiBulkCommand { + public function getCommandId() { return 'APPEND'; } +} + +class Predis_Commands_SetRange extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SETRANGE'; } +} + +class Predis_Commands_Substr extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SUBSTR'; } +} + +class Predis_Commands_SetBit extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SETBIT'; } +} + +class Predis_Commands_GetBit extends Predis_MultiBulkCommand { + public function getCommandId() { return 'GETBIT'; } +} + +class Predis_Commands_Strlen extends Predis_MultiBulkCommand { + public function getCommandId() { return 'STRLEN'; } +} + +/* commands operating on the key space */ +class Predis_Commands_Keys extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'KEYS'; } +} + +class Predis_Commands_Keys_v1_2 extends Predis_Commands_Keys { + public function parseResponse($data) { + return explode(' ', $data); + } +} + +class Predis_Commands_RandomKey extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'RANDOMKEY'; } + public function parseResponse($data) { return $data !== '' ? $data : null; } +} + +class Predis_Commands_Rename extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'RENAME'; } +} + +class Predis_Commands_RenamePreserve extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'RENAMENX'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_Expire extends Predis_MultiBulkCommand { + public function getCommandId() { return 'EXPIRE'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_ExpireAt extends Predis_MultiBulkCommand { + public function getCommandId() { return 'EXPIREAT'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_Persist extends Predis_MultiBulkCommand { + public function getCommandId() { return 'PERSIST'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_DatabaseSize extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'DBSIZE'; } +} + +class Predis_Commands_TimeToLive extends Predis_MultiBulkCommand { + public function getCommandId() { return 'TTL'; } +} + +/* commands operating on lists */ +class Predis_Commands_ListPushTail extends Predis_MultiBulkCommand { + public function getCommandId() { return 'RPUSH'; } +} + +class Predis_Commands_ListPushTailX extends Predis_MultiBulkCommand { + public function getCommandId() { return 'RPUSHX'; } +} + +class Predis_Commands_ListPushHead extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LPUSH'; } +} + +class Predis_Commands_ListPushHeadX extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LPUSHX'; } +} + +class Predis_Commands_ListLength extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LLEN'; } +} + +class Predis_Commands_ListRange extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LRANGE'; } +} + +class Predis_Commands_ListTrim extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LTRIM'; } +} + +class Predis_Commands_ListIndex extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LINDEX'; } +} + +class Predis_Commands_ListSet extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LSET'; } +} + +class Predis_Commands_ListRemove extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LREM'; } +} + +class Predis_Commands_ListPopLastPushHead extends Predis_MultiBulkCommand { + public function getCommandId() { return 'RPOPLPUSH'; } +} + +class Predis_Commands_ListPopLastPushHeadBlocking extends Predis_MultiBulkCommand { + public function getCommandId() { return 'BRPOPLPUSH'; } +} + +class Predis_Commands_ListPopFirst extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LPOP'; } +} + +class Predis_Commands_ListPopLast extends Predis_MultiBulkCommand { + public function getCommandId() { return 'RPOP'; } +} + +class Predis_Commands_ListPopFirstBlocking extends Predis_MultiBulkCommand { + public function getCommandId() { return 'BLPOP'; } +} + +class Predis_Commands_ListPopLastBlocking extends Predis_MultiBulkCommand { + public function getCommandId() { return 'BRPOP'; } +} + +class Predis_Commands_ListInsert extends Predis_MultiBulkCommand { + public function getCommandId() { return 'LINSERT'; } +} + +/* commands operating on sets */ +class Predis_Commands_SetAdd extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SADD'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_SetRemove extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SREM'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_SetPop extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SPOP'; } +} + +class Predis_Commands_SetMove extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SMOVE'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_SetCardinality extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SCARD'; } +} + +class Predis_Commands_SetIsMember extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SISMEMBER'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_SetIntersection extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SINTER'; } + public function filterArguments(Array $arguments) { + return Predis_Shared_Utils::filterArrayArguments($arguments); + } +} + +class Predis_Commands_SetIntersectionStore extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SINTERSTORE'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 2 && is_array($arguments[1])) { + return array_merge(array($arguments[0]), $arguments[1]); + } + return $arguments; + } +} + +class Predis_Commands_SetUnion extends Predis_Commands_SetIntersection { + public function getCommandId() { return 'SUNION'; } +} + +class Predis_Commands_SetUnionStore extends Predis_Commands_SetIntersectionStore { + public function getCommandId() { return 'SUNIONSTORE'; } +} + +class Predis_Commands_SetDifference extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SDIFF'; } +} + +class Predis_Commands_SetDifferenceStore extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SDIFFSTORE'; } +} + +class Predis_Commands_SetMembers extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SMEMBERS'; } +} + +class Predis_Commands_SetRandomMember extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SRANDMEMBER'; } +} + +/* commands operating on sorted sets */ +class Predis_Commands_ZSetAdd extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZADD'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_ZSetIncrementBy extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZINCRBY'; } +} + +class Predis_Commands_ZSetRemove extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZREM'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_ZSetUnionStore extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZUNIONSTORE'; } + public function filterArguments(Array $arguments) { + $options = array(); + $argc = count($arguments); + if ($argc > 2 && is_array($arguments[$argc - 1])) { + $options = $this->prepareOptions(array_pop($arguments)); + } + if (is_array($arguments[1])) { + $arguments = array_merge( + array($arguments[0], count($arguments[1])), + $arguments[1] + ); + } + return array_merge($arguments, $options); + } + private function prepareOptions($options) { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = array(); + if (isset($opts['WEIGHTS']) && is_array($opts['WEIGHTS'])) { + $finalizedOpts[] = 'WEIGHTS'; + foreach ($opts['WEIGHTS'] as $weight) { + $finalizedOpts[] = $weight; + } + } + if (isset($opts['AGGREGATE'])) { + $finalizedOpts[] = 'AGGREGATE'; + $finalizedOpts[] = $opts['AGGREGATE']; + } + return $finalizedOpts; + } +} + +class Predis_Commands_ZSetIntersectionStore extends Predis_Commands_ZSetUnionStore { + public function getCommandId() { return 'ZINTERSTORE'; } +} + +class Predis_Commands_ZSetRange extends Predis_MultiBulkCommand { + private $_withScores = false; + public function getCommandId() { return 'ZRANGE'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 4) { + $lastType = gettype($arguments[3]); + if ($lastType === 'string' && strtolower($arguments[3]) === 'withscores') { + // used for compatibility with older versions + $arguments[3] = array('WITHSCORES' => true); + $lastType = 'array'; + } + if ($lastType === 'array') { + $options = $this->prepareOptions(array_pop($arguments)); + return array_merge($arguments, $options); + } + } + return $arguments; + } + protected function prepareOptions($options) { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = array(); + if (isset($opts['WITHSCORES'])) { + $finalizedOpts[] = 'WITHSCORES'; + $this->_withScores = true; + } + return $finalizedOpts; + } + public function parseResponse($data) { + if ($this->_withScores) { + if ($data instanceof Iterator) { + return new Predis_Shared_MultiBulkResponseKVIterator($data); + } + $result = array(); + for ($i = 0; $i < count($data); $i++) { + $result[] = array($data[$i], $data[++$i]); + } + return $result; + } + return $data; + } +} + +class Predis_Commands_ZSetReverseRange extends Predis_Commands_ZSetRange { + public function getCommandId() { return 'ZREVRANGE'; } +} + +class Predis_Commands_ZSetRangeByScore extends Predis_Commands_ZSetRange { + public function getCommandId() { return 'ZRANGEBYSCORE'; } + protected function prepareOptions($options) { + $opts = array_change_key_case($options, CASE_UPPER); + $finalizedOpts = array(); + if (isset($opts['LIMIT']) && is_array($opts['LIMIT'])) { + $limit = array_change_key_case($opts['LIMIT'], CASE_UPPER); + $finalizedOpts[] = 'LIMIT'; + $finalizedOpts[] = isset($limit['OFFSET']) ? $limit['OFFSET'] : $limit[0]; + $finalizedOpts[] = isset($limit['COUNT']) ? $limit['COUNT'] : $limit[1]; + } + return array_merge($finalizedOpts, parent::prepareOptions($options)); + } +} + +class Predis_Commands_ZSetReverseRangeByScore extends Predis_Commands_ZSetRangeByScore { + public function getCommandId() { return 'ZREVRANGEBYSCORE'; } +} + +class Predis_Commands_ZSetCount extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZCOUNT'; } +} + +class Predis_Commands_ZSetCardinality extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZCARD'; } +} + +class Predis_Commands_ZSetScore extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZSCORE'; } +} + +class Predis_Commands_ZSetRemoveRangeByScore extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZREMRANGEBYSCORE'; } +} + +class Predis_Commands_ZSetRank extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZRANK'; } +} + +class Predis_Commands_ZSetReverseRank extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZREVRANK'; } +} + +class Predis_Commands_ZSetRemoveRangeByRank extends Predis_MultiBulkCommand { + public function getCommandId() { return 'ZREMRANGEBYRANK'; } +} + +/* commands operating on hashes */ +class Predis_Commands_HashSet extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HSET'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_HashSetPreserve extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HSETNX'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_HashSetMultiple extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HMSET'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 2 && is_array($arguments[1])) { + $flattenedKVs = array($arguments[0]); + $args = &$arguments[1]; + foreach ($args as $k => $v) { + $flattenedKVs[] = $k; + $flattenedKVs[] = $v; + } + return $flattenedKVs; + } + return $arguments; + } +} + +class Predis_Commands_HashIncrementBy extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HINCRBY'; } +} + +class Predis_Commands_HashGet extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HGET'; } +} + +class Predis_Commands_HashGetMultiple extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HMGET'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 2 && is_array($arguments[1])) { + $flattenedKVs = array($arguments[0]); + $args = &$arguments[1]; + foreach ($args as $v) { + $flattenedKVs[] = $v; + } + return $flattenedKVs; + } + return $arguments; + } +} + +class Predis_Commands_HashDelete extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HDEL'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_HashExists extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HEXISTS'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_HashLength extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HLEN'; } +} + +class Predis_Commands_HashKeys extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HKEYS'; } +} + +class Predis_Commands_HashValues extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HVALS'; } +} + +class Predis_Commands_HashGetAll extends Predis_MultiBulkCommand { + public function getCommandId() { return 'HGETALL'; } + public function parseResponse($data) { + if ($data instanceof Iterator) { + return new Predis_Shared_MultiBulkResponseKVIterator($data); + } + $result = array(); + for ($i = 0; $i < count($data); $i++) { + $result[$data[$i]] = $data[++$i]; + } + return $result; + } +} + +/* multiple databases handling commands */ +class Predis_Commands_SelectDatabase extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SELECT'; } +} + +class Predis_Commands_MoveKey extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'MOVE'; } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_FlushDatabase extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'FLUSHDB'; } +} + +class Predis_Commands_FlushAll extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'FLUSHALL'; } +} + +/* sorting */ +class Predis_Commands_Sort extends Predis_MultiBulkCommand { + public function getCommandId() { return 'SORT'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 1) { + return $arguments; + } + + $query = array($arguments[0]); + $sortParams = array_change_key_case($arguments[1], CASE_UPPER); + + if (isset($sortParams['BY'])) { + $query[] = 'BY'; + $query[] = $sortParams['BY']; + } + if (isset($sortParams['GET'])) { + $getargs = $sortParams['GET']; + if (is_array($getargs)) { + foreach ($getargs as $getarg) { + $query[] = 'GET'; + $query[] = $getarg; + } + } + else { + $query[] = 'GET'; + $query[] = $getargs; + } + } + if (isset($sortParams['LIMIT']) && is_array($sortParams['LIMIT']) + && count($sortParams['LIMIT']) == 2) { + + $query[] = 'LIMIT'; + $query[] = $sortParams['LIMIT'][0]; + $query[] = $sortParams['LIMIT'][1]; + } + if (isset($sortParams['SORT'])) { + $query[] = strtoupper($sortParams['SORT']); + } + if (isset($sortParams['ALPHA']) && $sortParams['ALPHA'] == true) { + $query[] = 'ALPHA'; + } + if (isset($sortParams['STORE'])) { + $query[] = 'STORE'; + $query[] = $sortParams['STORE']; + } + + return $query; + } +} + +/* transactions */ +class Predis_Commands_Multi extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'MULTI'; } +} + +class Predis_Commands_Exec extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'EXEC'; } +} + +class Predis_Commands_Discard extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'DISCARD'; } +} + +/* publish/subscribe */ +class Predis_Commands_Subscribe extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SUBSCRIBE'; } + public function filterArguments(Array $arguments) { + return Predis_Shared_Utils::filterArrayArguments($arguments); + } +} + +class Predis_Commands_Unsubscribe extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'UNSUBSCRIBE'; } +} + +class Predis_Commands_SubscribeByPattern extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'UNSUBSCRIBE'; } + public function filterArguments(Array $arguments) { + return Predis_Shared_Utils::filterArrayArguments($arguments); + } +} + +class Predis_Commands_UnsubscribeByPattern extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'PUNSUBSCRIBE'; } +} + +class Predis_Commands_Publish extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'PUBLISH'; } +} + +class Predis_Commands_Watch extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'WATCH'; } + public function filterArguments(Array $arguments) { + if (isset($arguments[0]) && is_array($arguments[0])) { + return $arguments[0]; + } + return $arguments; + } + public function parseResponse($data) { return (bool) $data; } +} + +class Predis_Commands_Unwatch extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'UNWATCH'; } + public function parseResponse($data) { return (bool) $data; } +} + +/* persistence control commands */ +class Predis_Commands_Save extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SAVE'; } +} + +class Predis_Commands_BackgroundSave extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'BGSAVE'; } + public function parseResponse($data) { + if ($data == 'Background saving started') { + return true; + } + return $data; + } +} + +class Predis_Commands_BackgroundRewriteAppendOnlyFile extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'BGREWRITEAOF'; } + public function parseResponse($data) { + return $data == 'Background append only file rewriting started'; + } +} + +class Predis_Commands_LastSave extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'LASTSAVE'; } +} + +class Predis_Commands_Shutdown extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SHUTDOWN'; } + public function closesConnection() { return true; } +} + +/* remote server control commands */ +class Predis_Commands_Info extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'INFO'; } + public function parseResponse($data) { + $info = array(); + $infoLines = explode("\r\n", $data, -1); + foreach ($infoLines as $row) { + @list($k, $v) = explode(':', $row); + if ($row === '' || !isset($v)) { + continue; + } + if (!preg_match('/^db\d+$/', $k)) { + if ($k === 'allocation_stats') { + $info[$k] = $this->parseAllocationStats($v); + continue; + } + $info[$k] = $v; + } + else { + $info[$k] = $this->parseDatabaseStats($v); + } + } + return $info; + } + protected function parseDatabaseStats($str) { + $db = array(); + foreach (explode(',', $str) as $dbvar) { + list($dbvk, $dbvv) = explode('=', $dbvar); + $db[trim($dbvk)] = $dbvv; + } + return $db; + } + protected function parseAllocationStats($str) { + $stats = array(); + foreach (explode(',', $str) as $kv) { + @list($size, $objects, $extra) = explode('=', $kv); + // hack to prevent incorrect values when parsing the >=256 key + if (isset($extra)) { + $size = ">=$objects"; + $objects = $extra; + } + $stats[$size] = $objects; + } + return $stats; + } +} + +class Predis_Commands_Info_v24 extends Predis_Commands_Info { + public function parseResponse($data) { + $info = array(); + $current = null; + $infoLines = explode("\r\n", $data, -1); + foreach ($infoLines as $row) { + if ($row === '') { + continue; + } + if (preg_match('/^# (\w+)$/', $row, $matches)) { + $info[$matches[1]] = array(); + $current = &$info[$matches[1]]; + continue; + } + list($k, $v) = explode(':', $row); + if (!preg_match('/^db\d+$/', $k)) { + if ($k === 'allocation_stats') { + $current[$k] = $this->parseAllocationStats($v); + continue; + } + $current[$k] = $v; + } + else { + $current[$k] = $this->parseDatabaseStats($v); + } + } + return $info; + } +} + +class Predis_Commands_SlaveOf extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'SLAVEOF'; } + public function filterArguments(Array $arguments) { + if (count($arguments) === 0 || $arguments[0] === 'NO ONE') { + return array('NO', 'ONE'); + } + return $arguments; + } +} + +class Predis_Commands_Config extends Predis_MultiBulkCommand { + public function canBeHashed() { return false; } + public function getCommandId() { return 'CONFIG'; } +} +?> diff --git a/src/protected/lib/cache/RedisFactory.php b/src/protected/lib/cache/RedisFactory.php new file mode 100644 index 0000000..fe44287 --- /dev/null +++ b/src/protected/lib/cache/RedisFactory.php @@ -0,0 +1,59 @@ + +* +* @changelogs +* 151025:1 INIT version. +*/ + +namespace SaltFish; + +/** + * class RedisFactory + */ +class RedisFactory{ + + private static $instance; + public $h; + public $pool = array(); + + + /** + * [__construct description] + * @param array &$h, redis all config + */ + private function __construct(){ + $this->h = Config::get('cache'); + return; + } + + /** + * [getInstance description] + * @return [type] [description] + */ + public static function getInstance(){ + if(empty(self::$instance)){ + self::$instance = new RedisFactory; + } + return self::$instance; + } + + /** + * [spawn description] + * @param string $name, redis instance name + * @return RedisProxy RedisProxy + */ + public static function &spawn($name){ + $instance = self::getInstance(); + // if alerady spawn return pool result + if(!empty($instance->pool[$name])) return $instance->pool[$name]; + // spawn + $instance->pool[$name] = new RedisProxy($instance->h[$name]); + return $instance->pool[$name]; + } + + +} \ No newline at end of file diff --git a/src/protected/lib/cache/RedisProxy.php b/src/protected/lib/cache/RedisProxy.php new file mode 100644 index 0000000..1aabdff --- /dev/null +++ b/src/protected/lib/cache/RedisProxy.php @@ -0,0 +1,104 @@ + +* +* @changelogs +* 141109:1 INIT version. +* +*/ + +namespace SaltFish; + +/** +* CLASS RedisProxy +*/ +class RedisProxy{ + + public $RedisInstance; + public $connectInfo; + + + /** + * [__construct description] + * @param array &$connectInfo, e.g. array(host=>'', port=>3306, pass=>'', database=>0). + * @return boolean connect result. + */ + public function __construct(&$connectInfo){ + $this->connectInfo = $connectInfo; + $this->RedisInstance = new \Redis; + return; + } + + /** + * [__destruct description] + */ + public function __destruct(){ + return $this->disconnect(); + } + + /** + * [__call description] + * @param string $name, called function name + * @param array $args, input args + * @return mixed function call return + */ + public function __call($name, $args){ + // check connection + if(!$this->connect()) return false; + // call instance + $argnum = count($args); + if($argnum === 1){ + return $this->RedisInstance->$name($args[0]); + }elseif($argnum === 2){ + return $this->RedisInstance->$name($args[0], $args[1]); + }elseif($argnum === 3){ + return $this->RedisInstance->$name($args[0], $args[1], $args[2]); + }elseif($argnum === 4){ + return $this->RedisInstance->$name($args[0], $args[1], $args[2], $args[3]); + } + return false; + } + + /** + * [getName description] + * @return string name + */ + public function getName(){ + return $this->connectInfo['name']; + } + + /** + * [connect description] + * @return boolean success + */ + public function connect(){ + $info = &$this->connectInfo; + if(!$this->RedisInstance->IsConnected()){ + if(!$this->RedisInstance->connect( + $info['host'], $info['port'], $info['timeout'] + )) return false; + // auth and select database + if(!empty($info['pass'])){ + if(!$this->RedisInstance->auth($info['pass'])) return false; + } + return $this->RedisInstance->select($info['database']); + } + return true; + } + + /** + * [disconnect description] + * @return boolean result + */ + public function disconnect(){ + if($this->RedisInstance->IsConnected()){ + return $this->RedisInstance->close(); + } + return true; + } + + +} diff --git a/src/protected/lib/encrypt/Encrypt.php b/src/protected/lib/encrypt/Encrypt.php new file mode 100644 index 0000000..ec84e6d --- /dev/null +++ b/src/protected/lib/encrypt/Encrypt.php @@ -0,0 +1,159 @@ + +* +* @changelogs +* 161017:6 ADD matchMd5() method. +* 160907:5 ADD const. +* 160901:4 ADD getRamdomString() method. +* 160823:3 MAKE iv readable. +* 160130:2 ADD opensslRandomPseudoBytes(); +* 141118:1 ADD hash(), match(), pbkdf2() method. +* +*/ + +namespace SaltFish; + +/** + * CLASS Encrypt + */ +class Encrypt{ + // consts + const HASH_ALGORITHM = "SHA512"; + const IV_LEN = 16; + const HASH_COUNT = 1024; + const OUTPUT_KEY_LEN = 64; + const MD5_LEN = 32; + const MD5_KEY_LEN = 48; + const IV_SOURCE = MCRYPT_DEV_URANDOM; + const MCRYPT_MODE = MCRYPT_MODE_CFB; + const MCRYPT_SIZE = MCRYPT_CAST_256; + + /** + * [getRamdomString description] + * @param [type] $len [description] + * @return [type] [description] + */ + public static function getRamdomString($len){ + $size = mcrypt_get_iv_size(self::MCRYPT_SIZE, self::MCRYPT_MODE); + return substr(bin2hex(mcrypt_create_iv($size, self::IV_SOURCE)), 0, $len); + } + + /** + * [hash description] + * @param [type] $_i [description] + * @return [type] [description] + */ + public static function hash($_i, $iv = ""){ + if(empty($iv)){ + $iv = self::getRamdomString(self::IV_LEN); + } + $hash = self::pbkdf2(self::HASH_ALGORITHM, $_i, $iv, self::HASH_COUNT, self::OUTPUT_KEY_LEN); + return $hash.$iv; + } + + /** + * [match description] + * @param [type] $_i [description] + * @param [type] $_hash [description] + * @return [type] [description] + */ + public static function match($_i, $_hash){ + $hash = substr($_hash, 0, self::OUTPUT_KEY_LEN-self::IV_LEN); + $iv = substr($_hash, self::OUTPUT_KEY_LEN-self::IV_LEN); + $newHash = self::pbkdf2(self::HASH_ALGORITHM, $_i, $iv, self::HASH_COUNT, self::OUTPUT_KEY_LEN); + if($hash === $newHash){ + return true; + } + return false; + } + + /** + * [matchMd5 description] + * @param [type] $_i [description] + * @param [type] $_hash [description] + * @return [type] [description] + */ + public static function matchMd5($_i, $_hash){ + $hash = substr($_hash, 0, self::MD5_KEY_LEN-self::IV_LEN); + $iv = substr($_hash, self::MD5_KEY_LEN-self::IV_LEN); + $newHash = md5($_i.$iv); + if($hash === $newHash){ + return true; + } + return false; + } + + + /** + * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt + * $algorithm - The hash algorithm to use. Recommended: SHA256 + * $password - The password. + * $salt - A salt that is unique to the password. + * $count - Iteration count. Higher is better, but slower. Recommended: At least 1000. + * $key_length - The length of the derived key in bytes. + * $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise. + * Returns: A $key_length-byte key derived from the password and salt. + * + * Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt + * + * This implementation of PBKDF2 was originally created by https://defuse.ca + * With improvements by http://www.variations-of-shadow.com + */ + private static function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false){ + $key_length = $key_length - self::IV_LEN; // iv len is usually 16 + $algorithm = strtolower($algorithm); + if(!in_array($algorithm, hash_algos(), true)) + trigger_error('PBKDF2 ERROR: Invalid hash algorithm.', E_USER_ERROR); + if($count <= 0 || $key_length <= 0) + trigger_error('PBKDF2 ERROR: Invalid parameters.', E_USER_ERROR); + + if(function_exists("hash_pbkdf2")){ + // The output length is in NIBBLES (4-bits) if $raw_output is false! + if (!$raw_output) { + // $key_length = $key_length * 2; + } + return hash_pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output); + } + + $hash_length = strlen(hash($algorithm, "", true)); + $block_count = ceil($key_length / $hash_length); + + $output = ""; + for($i = 1; $i <= $block_count; $i++) { + // $i encoded as 4 bytes, big endian. + $last = $salt . pack("N", $i); + // first iteration + $last = $xorsum = hash_hmac($algorithm, $last, $password, true); + // perform the other $count - 1 iterations + for ($j = 1; $j < $count; $j++) { + $xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true)); + } + $output .= $xorsum; + } + if($raw_output){ + return substr($output, 0, $key_length); + }else{ + return bin2hex(substr($output, 0, $key_length)); + } + } + + /** + * [opensslRandomPseudoBytes description] + * @param [type] $length [description] + * @return [type] [description] + */ + public static function opensslRandomPseudoBytes($length, $raw_output = false) { + $length_n = (int) $length; // shell injection is no fun + if(!$raw_output) $length_n = $length_n/2; + $handle = popen("/usr/bin/openssl rand $length_n", "r"); + $data = stream_get_contents($handle); + pclose($handle); + if($raw_output) return $data; + return bin2hex($data); + } +} + diff --git a/src/protected/lib/encrypt/Encrypt.test.php b/src/protected/lib/encrypt/Encrypt.test.php new file mode 100644 index 0000000..7f09a30 --- /dev/null +++ b/src/protected/lib/encrypt/Encrypt.test.php @@ -0,0 +1,7 @@ +size is the "real" number of bytes the dom was created from. + * but for most purposes, it's a really good estimation. + * Paperg - Added the forceTagsClosed to the dom constructor. Forcing tags closed is great for malformed html, but it CAN lead to parsing errors. + * Allow the user to tell us how much they trust the html. + * Paperg add the text and plaintext to the selectors for the find syntax. plaintext implies text in the innertext of a node. text implies that the tag is a text node. + * This allows for us to find tags based on the text they contain. + * Create find_ancestor_tag to see if a tag is - at any level - inside of another specific tag. + * Paperg: added parse_charset so that we know about the character set of the source document. + * NOTE: If the user's system has a routine called get_last_retrieve_url_contents_content_type availalbe, we will assume it's returning the content-type header from the + * last transfer or curl_exec, and we will parse that and use it in preference to any other method of charset detection. + * + * Licensed under The MIT License + * Redistributions of files must retain the above copyright notice. + * + * @author S.C. Chen + * @author John Schlick + * @author Rus Carroll + * @version 1.11 ($Rev: 184 $) + * @package PlaceLocalInclude + * @subpackage simple_html_dom + */ + +/** + * All of the Defines for the classes below. + * @author S.C. Chen + */ +define('HDOM_TYPE_ELEMENT', 1); +define('HDOM_TYPE_COMMENT', 2); +define('HDOM_TYPE_TEXT', 3); +define('HDOM_TYPE_ENDTAG', 4); +define('HDOM_TYPE_ROOT', 5); +define('HDOM_TYPE_UNKNOWN', 6); +define('HDOM_QUOTE_DOUBLE', 0); +define('HDOM_QUOTE_SINGLE', 1); +define('HDOM_QUOTE_NO', 3); +define('HDOM_INFO_BEGIN', 0); +define('HDOM_INFO_END', 1); +define('HDOM_INFO_QUOTE', 2); +define('HDOM_INFO_SPACE', 3); +define('HDOM_INFO_TEXT', 4); +define('HDOM_INFO_INNER', 5); +define('HDOM_INFO_OUTER', 6); +define('HDOM_INFO_ENDSPACE',7); +define('DEFAULT_TARGET_CHARSET', 'UTF-8'); +define('DEFAULT_BR_TEXT', "\r\n"); +// helper functions +// ----------------------------------------------------------------------------- +// get html dom from file +// $maxlen is defined in the code as PHP_STREAM_COPY_ALL which is defined as -1. +function file_get_html($url, $use_include_path = false, $context=null, $offset = -1, $maxLen=-1, $lowercase = true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) +{ + // We DO force the tags to be terminated. + $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $defaultBRText); + // For sourceforge users: uncomment the next line and comment the retreive_url_contents line 2 lines down if it is not already done. + $contents = file_get_contents($url, $use_include_path, $context, $offset); + // Paperg - use our own mechanism for getting the contents as we want to control the timeout. +// $contents = retrieve_url_contents($url); + if (empty($contents)) + { + return false; + } + // The second parameter can force the selectors to all be lowercase. + $dom->load($contents, $lowercase, $stripRN); + return $dom; +} + +// get html dom from string +function str_get_html($str, $lowercase=true, $forceTagsClosed=true, $target_charset = DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) +{ + $dom = new simple_html_dom(null, $lowercase, $forceTagsClosed, $target_charset, $defaultBRText); + if (empty($str)) + { + $dom->clear(); + return false; + } + $dom->load($str, $lowercase, $stripRN); + return $dom; +} + +// dump html dom tree +function dump_html_tree($node, $show_attr=true, $deep=0) +{ + $node->dump($node); +} + +/** + * simple html dom node + * PaperG - added ability for "find" routine to lowercase the value of the selector. + * PaperG - added $tag_start to track the start position of the tag in the total byte index + * + * @package PlaceLocalInclude + */ +class simple_html_dom_node { + public $nodetype = HDOM_TYPE_TEXT; + public $tag = 'text'; + public $attr = array(); + public $children = array(); + public $nodes = array(); + public $parent = null; + public $_ = array(); + public $tag_start = 0; + private $dom = null; + + function __construct($dom) + { + $this->dom = $dom; + $dom->nodes[] = $this; + } + + function __destruct() + { + $this->clear(); + } + + function __toString() + { + return $this->outertext(); + } + + // clean up memory due to php5 circular references memory leak... + function clear() + { + $this->dom = null; + $this->nodes = null; + $this->parent = null; + $this->children = null; + } + + // dump node's tree + function dump($show_attr=true, $deep=0) + { + $lead = str_repeat(' ', $deep); + + echo $lead.$this->tag; + if ($show_attr && count($this->attr)>0) + { + echo '('; + foreach ($this->attr as $k=>$v) + echo "[$k]=>\"".$this->$k.'", '; + echo ')'; + } + echo "\n"; + + foreach ($this->nodes as $c) + $c->dump($show_attr, $deep+1); + } + + + // Debugging function to dump a single dom node with a bunch of information about it. + function dump_node() + { + echo $this->tag; + if (count($this->attr)>0) + { + echo '('; + foreach ($this->attr as $k=>$v) + { + echo "[$k]=>\"".$this->$k.'", '; + } + echo ')'; + } + if (count($this->attr)>0) + { + echo ' $_ ('; + foreach ($this->_ as $k=>$v) + { + if (is_array($v)) + { + echo "[$k]=>("; + foreach ($v as $k2=>$v2) + { + echo "[$k2]=>\"".$v2.'", '; + } + echo ")"; + } else { + echo "[$k]=>\"".$v.'", '; + } + } + echo ")"; + } + + if (isset($this->text)) + { + echo " text: (" . $this->text . ")"; + } + + echo " children: " . count($this->children); + echo " nodes: " . count($this->nodes); + echo " tag_start: " . $this->tag_start; + echo "\n"; + + } + + // returns the parent of node + function parent() + { + return $this->parent; + } + + // returns children of node + function children($idx=-1) + { + if ($idx===-1) return $this->children; + if (isset($this->children[$idx])) return $this->children[$idx]; + return null; + } + + // returns the first child of node + function first_child() + { + if (count($this->children)>0) return $this->children[0]; + return null; + } + + // returns the last child of node + function last_child() + { + if (($count=count($this->children))>0) return $this->children[$count-1]; + return null; + } + + // returns the next sibling of node + function next_sibling() + { + if ($this->parent===null) return null; + $idx = 0; + $count = count($this->parent->children); + while ($idx<$count && $this!==$this->parent->children[$idx]) + ++$idx; + if (++$idx>=$count) return null; + return $this->parent->children[$idx]; + } + + // returns the previous sibling of node + function prev_sibling() + { + if ($this->parent===null) return null; + $idx = 0; + $count = count($this->parent->children); + while ($idx<$count && $this!==$this->parent->children[$idx]) + ++$idx; + if (--$idx<0) return null; + return $this->parent->children[$idx]; + } + + // function to locate a specific ancestor tag in the path to the root. + function find_ancestor_tag($tag) + { + global $debugObject; + if (is_object($debugObject)) + { + $debugObject->debugLogEntry(1); + } + + // Start by including ourselves in the comparison. + $returnDom = $this; + + while (!is_null($returnDom)) + { + if (is_object($debugObject)) + { + $debugObject->debugLog(2, "Current tag is: " . $returnDom->tag); + } + + if ($returnDom->tag == $tag) + { + break; + } + $returnDom = $returnDom->parent; + } + return $returnDom; + } + + // get dom node's inner html + function innertext() + { + if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; + if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + + $ret = ''; + foreach ($this->nodes as $n) + $ret .= $n->outertext(); + return $ret; + } + + // get dom node's outer text (with tag) + function outertext() + { + global $debugObject; + if (is_object($debugObject)) + { + $text = ''; + if ($this->tag == 'text') + { + if (!empty($this->text)) + { + $text = " with text: " . $this->text; + } + } + $debugObject->debugLog(1, 'Innertext of tag: ' . $this->tag . $text); + } + + if ($this->tag==='root') return $this->innertext(); + + // trigger callback + if ($this->dom && $this->dom->callback!==null) + { + call_user_func_array($this->dom->callback, array($this)); + } + + if (isset($this->_[HDOM_INFO_OUTER])) return $this->_[HDOM_INFO_OUTER]; + if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + + // render begin tag + if ($this->dom && $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]) + { + $ret = $this->dom->nodes[$this->_[HDOM_INFO_BEGIN]]->makeup(); + } else { + $ret = ""; + } + + // render inner text + if (isset($this->_[HDOM_INFO_INNER])) + { + // If it's a br tag... don't return the HDOM_INNER_INFO that we may or may not have added. + if ($this->tag != "br") + { + $ret .= $this->_[HDOM_INFO_INNER]; + } + } else { + if ($this->nodes) + { + foreach ($this->nodes as $n) + { + $ret .= $this->convert_text($n->outertext()); + } + } + } + + // render end tag + if (isset($this->_[HDOM_INFO_END]) && $this->_[HDOM_INFO_END]!=0) + $ret .= 'tag.'>'; + return $ret; + } + + // get dom node's plain text + function text() + { + if (isset($this->_[HDOM_INFO_INNER])) return $this->_[HDOM_INFO_INNER]; + switch ($this->nodetype) + { + case HDOM_TYPE_TEXT: return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + case HDOM_TYPE_COMMENT: return ''; + case HDOM_TYPE_UNKNOWN: return ''; + } + if (strcasecmp($this->tag, 'script')===0) return ''; + if (strcasecmp($this->tag, 'style')===0) return ''; + + $ret = ''; + // In rare cases, (always node type 1 or HDOM_TYPE_ELEMENT - observed for some span tags, and some p tags) $this->nodes is set to NULL. + // NOTE: This indicates that there is a problem where it's set to NULL without a clear happening. + // WHY is this happening? + if (!is_null($this->nodes)) + { + foreach ($this->nodes as $n) + { + $ret .= $this->convert_text($n->text()); + } + } + return $ret; + } + + function xmltext() + { + $ret = $this->innertext(); + $ret = str_ireplace('', '', $ret); + return $ret; + } + + // build node's text with tag + function makeup() + { + // text, comment, unknown + if (isset($this->_[HDOM_INFO_TEXT])) return $this->dom->restore_noise($this->_[HDOM_INFO_TEXT]); + + $ret = '<'.$this->tag; + $i = -1; + + foreach ($this->attr as $key=>$val) + { + ++$i; + + // skip removed attribute + if ($val===null || $val===false) + continue; + + $ret .= $this->_[HDOM_INFO_SPACE][$i][0]; + //no value attr: nowrap, checked selected... + if ($val===true) + $ret .= $key; + else { + switch ($this->_[HDOM_INFO_QUOTE][$i]) + { + case HDOM_QUOTE_DOUBLE: $quote = '"'; break; + case HDOM_QUOTE_SINGLE: $quote = '\''; break; + default: $quote = ''; + } + $ret .= $key.$this->_[HDOM_INFO_SPACE][$i][1].'='.$this->_[HDOM_INFO_SPACE][$i][2].$quote.$val.$quote; + } + } + $ret = $this->dom->restore_noise($ret); + return $ret . $this->_[HDOM_INFO_ENDSPACE] . '>'; + } + + // find elements by css selector + //PaperG - added ability for find to lowercase the value of the selector. + function find($selector, $idx=null, $lowercase=false) + { + $selectors = $this->parse_selector($selector); + if (($count=count($selectors))===0) return array(); + $found_keys = array(); + + // find each selector + for ($c=0; $c<$count; ++$c) + { + // The change on the below line was documented on the sourceforge code tracker id 2788009 + // used to be: if (($levle=count($selectors[0]))===0) return array(); + if (($levle=count($selectors[$c]))===0) return array(); + if (!isset($this->_[HDOM_INFO_BEGIN])) return array(); + + $head = array($this->_[HDOM_INFO_BEGIN]=>1); + + // handle descendant selectors, no recursive! + for ($l=0; $l<$levle; ++$l) + { + $ret = array(); + foreach ($head as $k=>$v) + { + $n = ($k===-1) ? $this->dom->root : $this->dom->nodes[$k]; + //PaperG - Pass this optional parameter on to the seek function. + $n->seek($selectors[$c][$l], $ret, $lowercase); + } + $head = $ret; + } + + foreach ($head as $k=>$v) + { + if (!isset($found_keys[$k])) + $found_keys[$k] = 1; + } + } + + // sort keys + ksort($found_keys); + + $found = array(); + foreach ($found_keys as $k=>$v) + $found[] = $this->dom->nodes[$k]; + + // return nth-element or array + if (is_null($idx)) return $found; + else if ($idx<0) $idx = count($found) + $idx; + return (isset($found[$idx])) ? $found[$idx] : null; + } + + // seek for given conditions + // PaperG - added parameter to allow for case insensitive testing of the value of a selector. + protected function seek($selector, &$ret, $lowercase=false) + { + global $debugObject; + if (is_object($debugObject)) + { + $debugObject->debugLogEntry(1); + } + + list($tag, $key, $val, $exp, $no_key) = $selector; + + // xpath index + if ($tag && $key && is_numeric($key)) + { + $count = 0; + foreach ($this->children as $c) + { + if ($tag==='*' || $tag===$c->tag) { + if (++$count==$key) { + $ret[$c->_[HDOM_INFO_BEGIN]] = 1; + return; + } + } + } + return; + } + + $end = (!empty($this->_[HDOM_INFO_END])) ? $this->_[HDOM_INFO_END] : 0; + if ($end==0) { + $parent = $this->parent; + while (!isset($parent->_[HDOM_INFO_END]) && $parent!==null) { + $end -= 1; + $parent = $parent->parent; + } + $end += $parent->_[HDOM_INFO_END]; + } + + for ($i=$this->_[HDOM_INFO_BEGIN]+1; $i<$end; ++$i) { + $node = $this->dom->nodes[$i]; + + $pass = true; + + if ($tag==='*' && !$key) { + if (in_array($node, $this->children, true)) + $ret[$i] = 1; + continue; + } + + // compare tag + if ($tag && $tag!=$node->tag && $tag!=='*') {$pass=false;} + // compare key + if ($pass && $key) { + if ($no_key) { + if (isset($node->attr[$key])) $pass=false; + } else { + if (($key != "plaintext") && !isset($node->attr[$key])) $pass=false; + } + } + // compare value + if ($pass && $key && $val && $val!=='*') { + // If they have told us that this is a "plaintext" search then we want the plaintext of the node - right? + if ($key == "plaintext") { + // $node->plaintext actually returns $node->text(); + $nodeKeyValue = $node->text(); + } else { + // this is a normal search, we want the value of that attribute of the tag. + $nodeKeyValue = $node->attr[$key]; + } + if (is_object($debugObject)) {$debugObject->debugLog(2, "testing node: " . $node->tag . " for attribute: " . $key . $exp . $val . " where nodes value is: " . $nodeKeyValue);} + + //PaperG - If lowercase is set, do a case insensitive test of the value of the selector. + if ($lowercase) { + $check = $this->match($exp, strtolower($val), strtolower($nodeKeyValue)); + } else { + $check = $this->match($exp, $val, $nodeKeyValue); + } + if (is_object($debugObject)) {$debugObject->debugLog(2, "after match: " . ($check ? "true" : "false"));} + + // handle multiple class + if (!$check && strcasecmp($key, 'class')===0) { + foreach (explode(' ',$node->attr[$key]) as $k) { + // Without this, there were cases where leading, trailing, or double spaces lead to our comparing blanks - bad form. + if (!empty($k)) { + if ($lowercase) { + $check = $this->match($exp, strtolower($val), strtolower($k)); + } else { + $check = $this->match($exp, $val, $k); + } + if ($check) break; + } + } + } + if (!$check) $pass = false; + } + if ($pass) $ret[$i] = 1; + unset($node); + } + // It's passed by reference so this is actually what this function returns. + if (is_object($debugObject)) {$debugObject->debugLog(1, "EXIT - ret: ", $ret);} + } + + protected function match($exp, $pattern, $value) { + global $debugObject; + if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} + + switch ($exp) { + case '=': + return ($value===$pattern); + case '!=': + return ($value!==$pattern); + case '^=': + return preg_match("/^".preg_quote($pattern,'/')."/", $value); + case '$=': + return preg_match("/".preg_quote($pattern,'/')."$/", $value); + case '*=': + if ($pattern[0]=='/') { + return preg_match($pattern, $value); + } + return preg_match("/".$pattern."/i", $value); + } + return false; + } + + protected function parse_selector($selector_string) { + global $debugObject; + if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} + + // pattern of CSS selectors, modified from mootools + // Paperg: Add the colon to the attrbute, so that it properly finds like google does. + // Note: if you try to look at this attribute, yo MUST use getAttribute since $dom->x:y will fail the php syntax check. +// Notice the \[ starting the attbute? and the @? following? This implies that an attribute can begin with an @ sign that is not captured. +// This implies that an html attribute specifier may start with an @ sign that is NOT captured by the expression. +// farther study is required to determine of this should be documented or removed. +// $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; + $pattern = "/([\w-:\*]*)(?:\#([\w-]+)|\.([\w-]+))?(?:\[@?(!?[\w-:]+)(?:([!*^$]?=)[\"']?(.*?)[\"']?)?\])?([\/, ]+)/is"; + preg_match_all($pattern, trim($selector_string).' ', $matches, PREG_SET_ORDER); + if (is_object($debugObject)) {$debugObject->debugLog(2, "Matches Array: ", $matches);} + + $selectors = array(); + $result = array(); + //print_r($matches); + + foreach ($matches as $m) { + $m[0] = trim($m[0]); + if ($m[0]==='' || $m[0]==='/' || $m[0]==='//') continue; + // for browser generated xpath + if ($m[1]==='tbody') continue; + + list($tag, $key, $val, $exp, $no_key) = array($m[1], null, null, '=', false); + if (!empty($m[2])) {$key='id'; $val=$m[2];} + if (!empty($m[3])) {$key='class'; $val=$m[3];} + if (!empty($m[4])) {$key=$m[4];} + if (!empty($m[5])) {$exp=$m[5];} + if (!empty($m[6])) {$val=$m[6];} + + // convert to lowercase + if ($this->dom->lowercase) {$tag=strtolower($tag); $key=strtolower($key);} + //elements that do NOT have the specified attribute + if (isset($key[0]) && $key[0]==='!') {$key=substr($key, 1); $no_key=true;} + + $result[] = array($tag, $key, $val, $exp, $no_key); + if (trim($m[7])===',') { + $selectors[] = $result; + $result = array(); + } + } + if (count($result)>0) + $selectors[] = $result; + return $selectors; + } + + function __get($name) { + if (isset($this->attr[$name])) + { + return $this->convert_text($this->attr[$name]); + } + switch ($name) { + case 'outertext': return $this->outertext(); + case 'innertext': return $this->innertext(); + case 'plaintext': return $this->text(); + case 'xmltext': return $this->xmltext(); + default: return array_key_exists($name, $this->attr); + } + } + + function __set($name, $value) { + switch ($name) { + case 'outertext': return $this->_[HDOM_INFO_OUTER] = $value; + case 'innertext': + if (isset($this->_[HDOM_INFO_TEXT])) return $this->_[HDOM_INFO_TEXT] = $value; + return $this->_[HDOM_INFO_INNER] = $value; + } + if (!isset($this->attr[$name])) { + $this->_[HDOM_INFO_SPACE][] = array(' ', '', ''); + $this->_[HDOM_INFO_QUOTE][] = HDOM_QUOTE_DOUBLE; + } + $this->attr[$name] = $value; + } + + function __isset($name) { + switch ($name) { + case 'outertext': return true; + case 'innertext': return true; + case 'plaintext': return true; + } + //no value attr: nowrap, checked selected... + return (array_key_exists($name, $this->attr)) ? true : isset($this->attr[$name]); + } + + function __unset($name) { + if (isset($this->attr[$name])) + unset($this->attr[$name]); + } + + // PaperG - Function to convert the text from one character set to another if the two sets are not the same. + function convert_text($text) { + global $debugObject; + if (is_object($debugObject)) {$debugObject->debugLogEntry(1);} + + $converted_text = $text; + + $sourceCharset = ""; + $targetCharset = ""; + if ($this->dom) { + $sourceCharset = strtoupper($this->dom->_charset); + $targetCharset = strtoupper($this->dom->_target_charset); + } + if (is_object($debugObject)) {$debugObject->debugLog(3, "source charset: " . $sourceCharset . " target charaset: " . $targetCharset);} + + if (!empty($sourceCharset) && !empty($targetCharset) && (strcasecmp($sourceCharset, $targetCharset) != 0)) + { + // Check if the reported encoding could have been incorrect and the text is actually already UTF-8 + if ((strcasecmp($targetCharset, 'UTF-8') == 0) && ($this->is_utf8($text))) + { + $converted_text = $text; + } + else + { + $converted_text = iconv($sourceCharset, $targetCharset, $text); + } + } + + return $converted_text; + } + + function is_utf8($string) + { + return (utf8_encode(utf8_decode($string)) == $string); + } + + // camel naming conventions + function getAllAttributes() {return $this->attr;} + function getAttribute($name) {return $this->__get($name);} + function setAttribute($name, $value) {$this->__set($name, $value);} + function hasAttribute($name) {return $this->__isset($name);} + function removeAttribute($name) {$this->__set($name, null);} + function getElementById($id) {return $this->find("#$id", 0);} + function getElementsById($id, $idx=null) {return $this->find("#$id", $idx);} + function getElementByTagName($name) {return $this->find($name, 0);} + function getElementsByTagName($name, $idx=null) {return $this->find($name, $idx);} + function parentNode() {return $this->parent();} + function childNodes($idx=-1) {return $this->children($idx);} + function firstChild() {return $this->first_child();} + function lastChild() {return $this->last_child();} + function nextSibling() {return $this->next_sibling();} + function previousSibling() {return $this->prev_sibling();} +} + +/** + * simple html dom parser + * Paperg - in the find routine: allow us to specify that we want case insensitive testing of the value of the selector. + * Paperg - change $size from protected to public so we can easily access it + * Paperg - added ForceTagsClosed in the constructor which tells us whether we trust the html or not. Default is to NOT trust it. + * + * @package PlaceLocalInclude + */ +class simple_html_dom { + public $root = null; + public $nodes = array(); + public $callback = null; + public $lowercase = false; + public $size; + protected $pos; + protected $doc; + protected $char; + protected $cursor; + protected $parent; + protected $noise = array(); + protected $token_blank = " \t\r\n"; + protected $token_equal = ' =/>'; + protected $token_slash = " />\r\n\t"; + protected $token_attr = ' >'; + protected $_charset = ''; + protected $_target_charset = ''; + protected $default_br_text = ""; + + // use isset instead of in_array, performance boost about 30%... + protected $self_closing_tags = array('img'=>1, 'br'=>1, 'input'=>1, 'meta'=>1, 'link'=>1, 'hr'=>1, 'base'=>1, 'embed'=>1, 'spacer'=>1); + protected $block_tags = array('root'=>1, 'body'=>1, 'form'=>1, 'div'=>1, 'span'=>1, 'table'=>1); + // Known sourceforge issue #2977341 + // B tags that are not closed cause us to return everything to the end of the document. + protected $optional_closing_tags = array( + 'tr'=>array('tr'=>1, 'td'=>1, 'th'=>1), + 'th'=>array('th'=>1), + 'td'=>array('td'=>1), + 'li'=>array('li'=>1), + 'dt'=>array('dt'=>1, 'dd'=>1), + 'dd'=>array('dd'=>1, 'dt'=>1), + 'dl'=>array('dd'=>1, 'dt'=>1), + 'p'=>array('p'=>1), + 'nobr'=>array('nobr'=>1), + 'b'=>array('b'=>1), + ); + + function __construct($str=null, $lowercase=true, $forceTagsClosed=true, $target_charset=DEFAULT_TARGET_CHARSET, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) { + if ($str) { + if (preg_match("/^http:\/\//i",$str) || is_file($str)) + $this->load_file($str); + else + $this->load($str, $lowercase, $stripRN, $defaultBRText); + } + // Forcing tags to be closed implies that we don't trust the html, but it can lead to parsing errors if we SHOULD trust the html. + if (!$forceTagsClosed) { + $this->optional_closing_array=array(); + } + $this->_target_charset = $target_charset; + } + + function __destruct() { + $this->clear(); + } + + // load html from string + function load($str, $lowercase=true, $stripRN=true, $defaultBRText=DEFAULT_BR_TEXT) { + global $debugObject; + + // prepare + $this->prepare($str, $lowercase, $stripRN, $defaultBRText); + // strip out comments + $this->remove_noise("''is"); + // strip out cdata + $this->remove_noise("''is", true); + // Per sourceforge http://sourceforge.net/tracker/?func=detail&aid=2949097&group_id=218559&atid=1044037 + // Script tags removal now preceeds style tag removal. + // strip out