commit d9e043c4685552117a768982c11e0dcf14b1ef48 Author: Mathieu Sanchez Date: Mon Nov 26 14:38:47 2018 +0900 First PUsh diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..55341e9 --- /dev/null +++ b/.htaccess @@ -0,0 +1,21 @@ +Options +FollowSymlinks + +RewriteEngine on +RewriteBase / + +RewriteCond %{ENV:REDIRECT_END} ^1$ +RewriteRule ^ - [L] + +# Redirection non-www vers www +# RewriteCond %{HTTP_HOST} ^eldotravo.fr$ +# RewriteRule ^(.*)$ http://www.eldotravo.fr/$1 [R=301,L,E=END:1] + +# Redirection des fichiers static pour contact.sanchez-mathieu.dev +RewriteCond %{HTTP_HOST} ^contact.sanchez-mathieu.dev$ +RewriteCond %{DOCUMENT_ROOT}/static%{REQUEST_URI} -f +RewriteRule ^(.*)$ static/$1 [L,E=END:1] + +# RewriteRule ^haute-garonne/toulouse/installation-entretien-climatisation /haute-garonne/toulouse/climatisation.php [L] + +# Redirection de toutes les requêtes vers Index.php +RewriteRule ^ index.php [L,E=END:1] \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..4f0611e --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..237ff2e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Web Project For CAU University 3 + +Write a web application My-Contacts satisfying the following requirements. + +- Information to be included: name, e-mail address, phone numbers, etc. +- Functions to be supported: insert, delete, update, search, list, etc. +- Server side scripting should be used. +- All data should be stored in DB. +- Build your own web servers. + +Submit the report describing the requirements, design, and testing results with source code. \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..fd92929 --- /dev/null +++ b/index.php @@ -0,0 +1,48 @@ + 1 && $pages[ count( $pages ) - 1 ] == '' ) { + $args = ( count( $parts ) > 1 ? '?' . $parts[1] : '' ); + header( $_SERVER['SERVER_PROTOCOL'] . ' 301 Moved Permanently' ); + header( 'Location: /' . rtrim( $urlA, '/' ) . $args ); + exit(); +} + +if ( $pages[0] == 'api' && isset( $pages[1] ) && preg_match( '#^([a-z]+)$#', $pages[1], $api1 ) && isset( $pages[2] ) && preg_match( '#^([a-z-]+)$#', $pages[2], $api2 ) ) { + new APIRouter( $api1[0], $api2[0] ); + +} else if ( preg_match( '#^test\.dev$#', $_SERVER['SERVER_NAME'] ) ) { + new SiteRouter( $pages ); +} else { + new Error( 404 ); +} + +/* +} elseif (preg_match('#^(www\.)?mvc\.eldotravo\.dev$#', $_SERVER['SERVER_NAME'])) { + new SiteRouter($pages); +*/ +?> \ No newline at end of file diff --git a/src/API/API.php b/src/API/API.php new file mode 100644 index 0000000..7f03283 --- /dev/null +++ b/src/API/API.php @@ -0,0 +1,29 @@ +declaredFunctions = $declaredFunctions; + } + + /** + * @return array + */ + public function getDeclaredFunctions() { + return $this->declaredFunctions; + } +} + +?> \ No newline at end of file diff --git a/src/API/APIError.php b/src/API/APIError.php new file mode 100644 index 0000000..fbd88b1 --- /dev/null +++ b/src/API/APIError.php @@ -0,0 +1,92 @@ + [ 'label' => '100 Continue', 'msg' => '100 Continue' ], + 101 => [ 'label' => '101 Switching Protocols', 'msg' => '101 Switching Protocols' ], + //Successful 2xx + 200 => [ 'label' => '200 OK', 'msg' => '200 OK' ], + 201 => [ 'label' => '201 Created', 'msg' => '201 Created' ], + 202 => [ 'label' => '202 Accepted', 'msg' => '202 Accepted' ], + 203 => [ 'label' => '203 Non-Authoritative Information', 'msg' => '203 Non-Authoritative Information' ], + 204 => [ 'label' => '204 No Content', 'msg' => '204 No Content' ], + 205 => [ 'label' => '205 Reset Content', 'msg' => '205 Reset Content' ], + 206 => [ 'label' => '206 Partial Content', 'msg' => '206 Partial Content' ], + 226 => [ 'label' => '226 IM Used', 'msg' => '226 IM Used' ], + //Redirection 3xx + 300 => [ 'label' => '300 Multiple Choices', 'msg' => '300 Multiple Choices' ], + 301 => [ 'label' => '301 Moved Permanently', 'msg' => '301 Moved Permanently' ], + 302 => [ 'label' => '302 Found', 'msg' => '302 Found' ], + 303 => [ 'label' => '303 See Other', 'msg' => '303 See Other' ], + 304 => [ 'label' => '304 Not Modified', 'msg' => '304 Not Modified' ], + 305 => [ 'label' => '305 Use Proxy', 'msg' => '305 Use Proxy' ], + 306 => [ 'label' => '306 (Unused)', 'msg' => '306 (Unused)' ], + 307 => [ 'label' => '307 Temporary Redirect', 'msg' => '307 Temporary Redirect' ], + //Client Error 4xx + 400 => [ 'label' => '400 Bad Request', 'msg' => '400 Bad Request' ], + 401 => [ 'label' => '401 Unauthorized', 'msg' => 'Vous n\'êtes pas autorisé à accéder à cette page' ], + 402 => [ 'label' => '402 Payment Required', 'msg' => '402 Payment Required' ], + 403 => [ 'label' => '403 Forbidden', 'msg' => '403 Forbidden' ], + 404 => [ 'label' => '404 Not Found', 'msg' => 'Page non trouvée' ], + 405 => [ 'label' => '405 Method Not Allowed', 'msg' => '405 Method Not Allowed' ], + 406 => [ 'label' => '406 Not Acceptable', 'msg' => '406 Not Acceptable' ], + 407 => [ 'label' => '407 Proxy Authentication Required', 'msg' => '407 Proxy Authentication Required' ], + 408 => [ 'label' => '408 Request Timeout', 'msg' => '408 Request Timeout' ], + 409 => [ 'label' => '409 Conflict', 'msg' => '409 Conflict' ], + 410 => [ 'label' => '410 Gone', 'msg' => '410 Gone' ], + 411 => [ 'label' => '411 Length Required', 'msg' => '411 Length Required' ], + 412 => [ 'label' => '412 Precondition Failed', 'msg' => '412 Precondition Failed' ], + 413 => [ 'label' => '413 Request Entity Too Large', 'msg' => '413 Request Entity Too Large' ], + 414 => [ 'label' => '414 Request-URI Too Long', 'msg' => '414 Request-URI Too Long' ], + 415 => [ 'label' => '415 Unsupported Media Type', 'msg' => '415 Unsupported Media Type' ], + 416 => [ 'label' => '416 Requested Range Not Satisfiable', 'msg' => '416 Requested Range Not Satisfiable' ], + 417 => [ 'label' => '417 Expectation Failed', 'msg' => '417 Expectation Failed' ], + 418 => [ 'label' => '418 I\'m a teapot', 'msg' => '418 I\'m a teapot' ], + 422 => [ 'label' => '422 Unprocessable Entity', 'msg' => '422 Unprocessable Entity' ], + 423 => [ 'label' => '423 Locked', 'msg' => '423 Locked' ], + 426 => [ 'label' => '426 Upgrade Required', 'msg' => '426 Upgrade Required' ], + 428 => [ 'label' => '428 Precondition Required', 'msg' => '428 Precondition Required' ], + 429 => [ 'label' => '429 Too Many Requests', 'msg' => '429 Too Many Requests' ], + 431 => [ 'label' => '431 Request Header Fields Too Large', 'msg' => '431 Request Header Fields Too Large' ], + //Server Error 5xx + 500 => [ 'label' => '500 Internal Server Error', 'msg' => 'Une erreur est survenue' ], + 501 => [ 'label' => '501 Not Implemented', 'msg' => '501 Not Implemented' ], + 502 => [ 'label' => '502 Bad Gateway', 'msg' => '502 Bad Gateway' ], + 503 => [ 'label' => '503 Service Unavailable', 'msg' => '503 Service Unavailable' ], + 504 => [ 'label' => '504 Gateway Timeout', 'msg' => '504 Gateway Timeout' ], + 505 => [ 'label' => '505 HTTP Version Not Supported', 'msg' => '505 HTTP Version Not Supported' ], + 506 => [ 'label' => '506 Variant Also Negotiates', 'msg' => '506 Variant Also Negotiates' ], + 510 => [ 'label' => '510 Not Extended', 'msg' => '510 Not Extended' ], + 511 => [ 'label' => '511 Network Authentication Required', 'msg' => '511 Network Authentication Required' ] + ]; + + header( 'Content-Type: application/json' ); + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $tabCode[ $ErrCode ]['label'] ); + echo json_encode( [ + 'status' => 'echec', + 'msg' => $publicMessage, + 'devMsg' => $devMessage, + 'code' => $code + ], JSON_PRETTY_PRINT ); + exit(); + } +} + +?> \ No newline at end of file diff --git a/src/API/APIRouter.php b/src/API/APIRouter.php new file mode 100644 index 0000000..375dfb5 --- /dev/null +++ b/src/API/APIRouter.php @@ -0,0 +1,132 @@ +getDeclaredFunctions() ) ) { + new APIError( 404 ); + } + + //On reconstruit le nom de la fonction + $array = explode( '-', $action ); + foreach ( $array as $key => $value ) { + if ( $key == 0 ) { + $array[ $key ] = $value; + } else { + $array[ $key ] = ucfirst( $value ); + } + } + $function = implode( '', $array ); //le nom de la fonction + + //On vérifie que la fonction existe dans la classe + if ( ! method_exists( $class, $function ) ) { + new APIError( 500, 'La fonction ' . $function . ' n\'existe pas dans la classe ' . get_class( $class ) ); + } + + //On vérifie que la méthode d'envoie est référencée + $method = $class->getDeclaredFunctions()[ $action ]['method']; + if ( ! in_array( $method, self::HTTP_METHODS ) ) { + new APIError( 500, 'méthode http inconnue' ); + } + + //On vérifie que la méthode requise et la mathode obtenue sont les même + if ( $method != $_SERVER['REQUEST_METHOD'] ) { + new APIError( 400, 'La méthode HTTP ne correspond pas à la méthode prévue' ); + } + + //On met les paramètres dans le tableau $data + $params = $class->getDeclaredFunctions()[ $action ]['params']; + $data = []; + if ( ! empty( $params ) ) { + if ( $method == 'DELETE' || $method == 'PUT' ) { + parse_str( file_get_contents( 'php://input' ), $data ); + } else if ( $method == 'POST' ) { + $data = $_POST; + } else if ( $method == 'GET' ) { + $data = $_GET; + } + + //On boucle sur les paramètres de la doc de la fonction + foreach ( $params as $p => $options ) { + if ( ! isset( $options['required'] ) ) { + $options['required'] = false; + } + //Si le paramètre est obligatoire et qu'il est vide ou non fourni on lève une erreur 400 BAD REQUEST + if ( $options['required'] && ( ! array_key_exists( $p, $data ) || ( empty( $data[ $p ] ) && $data[ $p ] != '0' ) ) ) { + $devMsg = 'Paramètre ' . $p . ' manquant'; + if ( isset( $options['devMsg'] ) ) { + $devMsg = $options['devMsg']; + } + $publicMsg = 'Des paramètres obligatoires ne sont pas envoyés ou sont vides'; + if ( isset( $options['publicMsg'] ) ) { + $publicMsg = $options['publicMsg']; + } + $code = ''; + if ( isset( $options['code'] ) ) { + $code = $options['code']; + } + new APIError( 400, $devMsg, $publicMsg, $code ); + } + + //On vérifie que le type donné correspond au typage requis + if ( isset( $options['type'] ) ) { + if ( $options['type'] == 'int' ) { + if ( ctype_digit( $data[ $p ] ) ) { + $data[ $p ] = (int) $data[ $p ]; + } else { + new APIError( 400, 'Le type donné ne correspond pas au type demandé pour le paramètre ' . $p . ' : string donné, ' . $options['type'] . ' requis' ); + } + } else if ( $options['type'] == 'bool' ) { + if ( $data[ $p ] == 'true' || $data[ $p ] == '1' ) { + $data[ $p ] = true; + } else if ( $data[ $p ] == 'false' || $data[ $p ] == '0' ) { + $data[ $p ] = false; + } else { + new APIError( 400, 'Le type donné ne correspond pas au type demandé pour le paramètre ' . $p . ' : string donné, ' . $options['type'] . ' requis' ); + } + } + } + + //Si un paramètre non obligatoire n'est pas donné par l'utilisateur on lui donne la valeur par défaut d'une chaine vide + if ( ! array_key_exists( $p, $data ) ) { + $data[ $p ] = ''; + } + } + } + + //On appelle la fonction correspondante pour le traitement + $class->$function( $data ); + } +} + +?> \ No newline at end of file diff --git a/src/Autoloader.php b/src/Autoloader.php new file mode 100644 index 0000000..659e755 --- /dev/null +++ b/src/Autoloader.php @@ -0,0 +1,24 @@ +data = []; + } + + protected function view() { + ob_start(); + extract( $this->data ); + require 'src/View/' . str_replace( '\\', '/', preg_replace( '#^' . Config::NAMESPACE . '\\\Controller\\\#', '', get_class( $this ) ) ) . '.php'; + ob_end_flush(); + exit(); + } + + protected function addData( $data ) { + $this->data += $data; + } + + protected function returnJson( $data ) { + header( 'Content-Type: application/json' ); + echo json_encode( $data ); + exit(); + } + + protected function throwError( $msg, $code = '' ) { + header( 'Content-Type: application/json' ); + echo json_encode( [ 'status' => 'echec', 'msg' => $msg, 'code' => $code ], JSON_PRETTY_PRINT ); + exit(); + } +} + +?> \ No newline at end of file diff --git a/src/Controller/ControllerSite.php b/src/Controller/ControllerSite.php new file mode 100644 index 0000000..1a3f798 --- /dev/null +++ b/src/Controller/ControllerSite.php @@ -0,0 +1,72 @@ +data = []; + $this->head = []; + $this->footer = []; + } + + + protected function view( $header = true, $footer = true ) { + ob_start(); + extract( $this->data ); + + if ( empty( $this->head['title'] ) ) { + $this->head['title'] = Config::TITLE_HEADER; + } + if ( empty( $this->head['description'] ) ) { + $this->head['description'] = Config::DESCRIPTION_HEADER; + } + if ( $header ) { + require 'src/View/Site/tpl/head.php'; + } + + require 'src/View/' . str_replace( '\\', '/', preg_replace( '#^' . Config::NAMESPACE . '\\\Controller\\\#', '', get_class( $this ) ) ) . '.php'; + + if ( $footer ) { + require 'src/View/Site/tpl/footer.php'; + } + + ob_end_flush(); + exit(); + } + + protected function addHead( $head ) { + $this->head += $head; + } + + protected function addData( $data ) { + $this->data += $data; + } + + protected function addFooter( $footer ) { + $this->footer += $footer; + } + + /** + * @param $data + */ + protected function returnJson( $data ) { + header( 'Content-Type: application/json' ); + echo json_encode( $data ); + exit(); + } + + protected function throwError( $msg, $code = '' ) { + header( 'Content-Type: application/json' ); + echo json_encode( [ 'status' => 'echec', 'msg' => $msg, 'code' => $code ], JSON_PRETTY_PRINT ); + exit(); + } +} + +?> \ No newline at end of file diff --git a/src/Controller/Error.php b/src/Controller/Error.php new file mode 100644 index 0000000..2ed3cbd --- /dev/null +++ b/src/Controller/Error.php @@ -0,0 +1,99 @@ + [ 'label' => '100 Continue', 'msg' => '100 Continue' ], + 101 => [ 'label' => '101 Switching Protocols', 'msg' => '101 Switching Protocols' ], + //Successful 2xx + 200 => [ 'label' => '200 OK', 'msg' => '200 OK' ], + 201 => [ 'label' => '201 Created', 'msg' => '201 Created' ], + 202 => [ 'label' => '202 Accepted', 'msg' => '202 Accepted' ], + 203 => [ 'label' => '203 Non-Authoritative Information', 'msg' => '203 Non-Authoritative Information' ], + 204 => [ 'label' => '204 No Content', 'msg' => '204 No Content' ], + 205 => [ 'label' => '205 Reset Content', 'msg' => '205 Reset Content' ], + 206 => [ 'label' => '206 Partial Content', 'msg' => '206 Partial Content' ], + 226 => [ 'label' => '226 IM Used', 'msg' => '226 IM Used' ], + //Redirection 3xx + 300 => [ 'label' => '300 Multiple Choices', 'msg' => '300 Multiple Choices' ], + 301 => [ 'label' => '301 Moved Permanently', 'msg' => '301 Moved Permanently' ], + 302 => [ 'label' => '302 Found', 'msg' => '302 Found' ], + 303 => [ 'label' => '303 See Other', 'msg' => '303 See Other' ], + 304 => [ 'label' => '304 Not Modified', 'msg' => '304 Not Modified' ], + 305 => [ 'label' => '305 Use Proxy', 'msg' => '305 Use Proxy' ], + 306 => [ 'label' => '306 (Unused)', 'msg' => '306 (Unused)' ], + 307 => [ 'label' => '307 Temporary Redirect', 'msg' => '307 Temporary Redirect' ], + //Client Error 4xx + 400 => [ 'label' => '400 Bad Request', 'msg' => '400 Bad Request' ], + 401 => [ 'label' => '401 Unauthorized', 'msg' => 'Vous n\'êtes pas autorisé à accéder à cette page' ], + 402 => [ 'label' => '402 Payment Required', 'msg' => '402 Payment Required' ], + 403 => [ 'label' => '403 Forbidden', 'msg' => '403 Forbidden' ], + 404 => [ 'label' => '404 Not Found', 'msg' => 'Page non trouvée' ], + 405 => [ 'label' => '405 Method Not Allowed', 'msg' => '405 Method Not Allowed' ], + 406 => [ 'label' => '406 Not Acceptable', 'msg' => '406 Not Acceptable' ], + 407 => [ 'label' => '407 Proxy Authentication Required', 'msg' => '407 Proxy Authentication Required' ], + 408 => [ 'label' => '408 Request Timeout', 'msg' => '408 Request Timeout' ], + 409 => [ 'label' => '409 Conflict', 'msg' => '409 Conflict' ], + 410 => [ 'label' => '410 Gone', 'msg' => '410 Gone' ], + 411 => [ 'label' => '411 Length Required', 'msg' => '411 Length Required' ], + 412 => [ 'label' => '412 Precondition Failed', 'msg' => '412 Precondition Failed' ], + 413 => [ 'label' => '413 Request Entity Too Large', 'msg' => '413 Request Entity Too Large' ], + 414 => [ 'label' => '414 Request-URI Too Long', 'msg' => '414 Request-URI Too Long' ], + 415 => [ 'label' => '415 Unsupported Media Type', 'msg' => '415 Unsupported Media Type' ], + 416 => [ 'label' => '416 Requested Range Not Satisfiable', 'msg' => '416 Requested Range Not Satisfiable' ], + 417 => [ 'label' => '417 Expectation Failed', 'msg' => '417 Expectation Failed' ], + 418 => [ 'label' => '418 I\'m a teapot', 'msg' => '418 I\'m a teapot' ], + 422 => [ 'label' => '422 Unprocessable Entity', 'msg' => '422 Unprocessable Entity' ], + 423 => [ 'label' => '423 Locked', 'msg' => '423 Locked' ], + 426 => [ 'label' => '426 Upgrade Required', 'msg' => '426 Upgrade Required' ], + 428 => [ 'label' => '428 Precondition Required', 'msg' => '428 Precondition Required' ], + 429 => [ 'label' => '429 Too Many Requests', 'msg' => '429 Too Many Requests' ], + 431 => [ 'label' => '431 Request Header Fields Too Large', 'msg' => '431 Request Header Fields Too Large' ], + //Server Error 5xx + 500 => [ 'label' => '500 Internal Server Error', 'msg' => 'Une erreur est survenue' ], + 501 => [ 'label' => '501 Not Implemented', 'msg' => '501 Not Implemented' ], + 502 => [ 'label' => '502 Bad Gateway', 'msg' => '502 Bad Gateway' ], + 503 => [ 'label' => '503 Service Unavailable', 'msg' => '503 Service Unavailable' ], + 504 => [ 'label' => '504 Gateway Timeout', 'msg' => '504 Gateway Timeout' ], + 505 => [ 'label' => '505 HTTP Version Not Supported', 'msg' => '505 HTTP Version Not Supported' ], + 506 => [ 'label' => '506 Variant Also Negotiates', 'msg' => '506 Variant Also Negotiates' ], + 510 => [ 'label' => '510 Not Extended', 'msg' => '510 Not Extended' ], + 511 => [ 'label' => '511 Network Authentication Required', 'msg' => '511 Network Authentication Required' ] + ]; + + if ( isset( $tabCode[ $ErrCode ] ) ) { + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $tabCode[ $ErrCode ]['label'] ); + if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) { + $this->throwError( $tabCode[ $ErrCode ]['msg'], 'Erreur_' . $ErrCode ); + } + $erreur = $tabCode[ $ErrCode ]['msg']; + $this->addData( [ 'erreur' => $erreur ] ); + } else { + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $tabCode[500]['label'] ); + if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) { + $this->throwError( $tabCode[500]['msg'], 'Erreur_500' ); + } + $erreur = $tabCode[500]['msg']; + $this->addData( [ 'erreur' => $erreur ] ); + } + + if ( ! empty( $message ) ) { + $this->addData( [ 'message' => $message ] ); + } + + $this->view(); + } +} \ No newline at end of file diff --git a/src/Controller/site/Index.php b/src/Controller/site/Index.php new file mode 100644 index 0000000..f56c0ff --- /dev/null +++ b/src/Controller/site/Index.php @@ -0,0 +1,27 @@ +addHead( [ + ] ); + + $this->addFooter( [ + + ] ); + + $this->addData( [] ); + $this->view(); + } +} + +?> \ No newline at end of file diff --git a/src/Controller/site/SiteError.php b/src/Controller/site/SiteError.php new file mode 100644 index 0000000..b1b8130 --- /dev/null +++ b/src/Controller/site/SiteError.php @@ -0,0 +1,106 @@ + [ 'label' => '100 Continue', 'msg' => '100 Continue' ], + 101 => [ 'label' => '101 Switching Protocols', 'msg' => '101 Switching Protocols' ], + //Successful 2xx + 200 => [ 'label' => '200 OK', 'msg' => '200 OK' ], + 201 => [ 'label' => '201 Created', 'msg' => '201 Created' ], + 202 => [ 'label' => '202 Accepted', 'msg' => '202 Accepted' ], + 203 => [ 'label' => '203 Non-Authoritative Information', 'msg' => '203 Non-Authoritative Information' ], + 204 => [ 'label' => '204 No Content', 'msg' => '204 No Content' ], + 205 => [ 'label' => '205 Reset Content', 'msg' => '205 Reset Content' ], + 206 => [ 'label' => '206 Partial Content', 'msg' => '206 Partial Content' ], + 226 => [ 'label' => '226 IM Used', 'msg' => '226 IM Used' ], + //Redirection 3xx + 300 => [ 'label' => '300 Multiple Choices', 'msg' => '300 Multiple Choices' ], + 301 => [ 'label' => '301 Moved Permanently', 'msg' => '301 Moved Permanently' ], + 302 => [ 'label' => '302 Found', 'msg' => '302 Found' ], + 303 => [ 'label' => '303 See Other', 'msg' => '303 See Other' ], + 304 => [ 'label' => '304 Not Modified', 'msg' => '304 Not Modified' ], + 305 => [ 'label' => '305 Use Proxy', 'msg' => '305 Use Proxy' ], + 306 => [ 'label' => '306 (Unused)', 'msg' => '306 (Unused)' ], + 307 => [ 'label' => '307 Temporary Redirect', 'msg' => '307 Temporary Redirect' ], + //Client Error 4xx + 400 => [ 'label' => '400 Bad Request', 'msg' => '400 Bad Request' ], + 401 => [ 'label' => '401 Unauthorized', 'msg' => '401 Unauthorized' ], + 402 => [ 'label' => '402 Payment Required', 'msg' => '402 Payment Required' ], + 403 => [ 'label' => '403 Forbidden', 'msg' => '403 Forbidden' ], + 404 => [ 'label' => '404 Not Found', 'msg' => 'Page non trouvée' ], + 405 => [ 'label' => '405 Method Not Allowed', 'msg' => '405 Method Not Allowed' ], + 406 => [ 'label' => '406 Not Acceptable', 'msg' => '406 Not Acceptable' ], + 407 => [ 'label' => '407 Proxy Authentication Required', 'msg' => '407 Proxy Authentication Required' ], + 408 => [ 'label' => '408 Request Timeout', 'msg' => '408 Request Timeout' ], + 409 => [ 'label' => '409 Conflict', 'msg' => '409 Conflict' ], + 410 => [ 'label' => '410 Gone', 'msg' => 'Cet artisan n\'est plus plus référencé sur Eldotravo' ], + 411 => [ 'label' => '411 Length Required', 'msg' => '411 Length Required' ], + 412 => [ 'label' => '412 Precondition Failed', 'msg' => '412 Precondition Failed' ], + 413 => [ 'label' => '413 Request Entity Too Large', 'msg' => '413 Request Entity Too Large' ], + 414 => [ 'label' => '414 Request-URI Too Long', 'msg' => '414 Request-URI Too Long' ], + 415 => [ 'label' => '415 Unsupported Media Type', 'msg' => '415 Unsupported Media Type' ], + 416 => [ 'label' => '416 Requested Range Not Satisfiable', 'msg' => '416 Requested Range Not Satisfiable' ], + 417 => [ 'label' => '417 Expectation Failed', 'msg' => '417 Expectation Failed' ], + 418 => [ 'label' => '418 I\'m a teapot', 'msg' => '418 I\'m a teapot' ], + 422 => [ 'label' => '422 Unprocessable Entity', 'msg' => '422 Unprocessable Entity' ], + 423 => [ 'label' => '423 Locked', 'msg' => '423 Locked' ], + 426 => [ 'label' => '426 Upgrade Required', 'msg' => '426 Upgrade Required' ], + 428 => [ 'label' => '428 Precondition Required', 'msg' => '428 Precondition Required' ], + 429 => [ 'label' => '429 Too Many Requests', 'msg' => '429 Too Many Requests' ], + 431 => [ 'label' => '431 Request Header Fields Too Large', 'msg' => '431 Request Header Fields Too Large' ], + //Server Error 5xx + 500 => [ 'label' => '500 Internal Server Error', 'msg' => 'Une erreur est survenue' ], + 501 => [ 'label' => '501 Not Implemented', 'msg' => '501 Not Implemented' ], + 502 => [ 'label' => '502 Bad Gateway', 'msg' => '502 Bad Gateway' ], + 503 => [ 'label' => '503 Service Unavailable', 'msg' => '503 Service Unavailable' ], + 504 => [ 'label' => '504 Gateway Timeout', 'msg' => '504 Gateway Timeout' ], + 505 => [ 'label' => '505 HTTP Version Not Supported', 'msg' => '505 HTTP Version Not Supported' ], + 506 => [ 'label' => '506 Variant Also Negotiates', 'msg' => '506 Variant Also Negotiates' ], + 510 => [ 'label' => '510 Not Extended', 'msg' => '510 Not Extended' ], + 511 => [ 'label' => '511 Network Authentication Required', 'msg' => '511 Network Authentication Required' ] + ]; + + if ( isset( $tabCode[ $ErrCode ] ) ) { + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $tabCode[ $ErrCode ]['label'] ); + if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) { + $this->throwError( 'Erreur ' . $tabCode[ $ErrCode ]['label'], 'Erreur_' . $ErrCode ); + } + $erreur = $tabCode[ $ErrCode ]['msg']; + $this->addData( [ 'erreur' => $erreur ] ); + } else { + header( $_SERVER['SERVER_PROTOCOL'] . ' ' . $tabCode[500]['label'] ); + if ( isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) && strtolower( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'xmlhttprequest' ) { + $this->throwError( 'Erreur ' . $tabCode[500]['label'], 'Erreur_500' ); + } + $erreur = $tabCode[500]['msg']; + $this->addData( [ 'erreur' => $erreur ] ); + } + + if ( ! empty( $message ) ) { + $this->addData( [ 'message' => $message ] ); + } + + $this->addHead( [ + 'title' => 'Un erreur est survenue', + 'description' => 'Oops une erreur est survenue', + 'robotNoIndex' => true + ] ); + + $this->view(); + } +} \ No newline at end of file diff --git a/src/Controller/site/SiteRouter.php b/src/Controller/site/SiteRouter.php new file mode 100644 index 0000000..f10df0b --- /dev/null +++ b/src/Controller/site/SiteRouter.php @@ -0,0 +1,24 @@ + \ No newline at end of file diff --git a/src/Model/BDD.php b/src/Model/BDD.php new file mode 100644 index 0000000..532830a --- /dev/null +++ b/src/Model/BDD.php @@ -0,0 +1,40 @@ + PDO::ERRMODE_WARNING, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC + ]; + + self::$bdd = new PDO( 'mysql:host=' . self::SQL_SERVER . ';dbname=' . self::SQL_DB . ';charset=utf8', + self::SQL_LOGIN, self::SQL_PASSWORD, $pdo_options ); + } catch ( Exception $e ) { + die( 'Erreur : ' . $e->getMessage() ); + } + } + + public static function instance() { + return self::$bdd; + } + + public static function lastInsertId() { + return self::$bdd->lastInsertId(); + } +} + +?> \ No newline at end of file diff --git a/src/Model/BDTables.php b/src/Model/BDTables.php new file mode 100644 index 0000000..aa46dc4 --- /dev/null +++ b/src/Model/BDTables.php @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/src/Model/FPDF.php b/src/Model/FPDF.php new file mode 100644 index 0000000..95c9a20 --- /dev/null +++ b/src/Model/FPDF.php @@ -0,0 +1,1913 @@ +_dochecks(); + // Initialization of properties + $this->state = 0; + $this->page = 0; + $this->n = 2; + $this->buffer = ''; + $this->pages = array(); + $this->PageInfo = array(); + $this->fonts = array(); + $this->FontFiles = array(); + $this->encodings = array(); + $this->cmaps = array(); + $this->images = array(); + $this->links = array(); + $this->InHeader = false; + $this->InFooter = false; + $this->lasth = 0; + $this->FontFamily = ''; + $this->FontStyle = ''; + $this->FontSizePt = 12; + $this->underline = false; + $this->DrawColor = '0 G'; + $this->FillColor = '0 g'; + $this->TextColor = '0 g'; + $this->ColorFlag = false; + $this->WithAlpha = false; + $this->ws = 0; + // Font path + if ( defined( 'FPDF_FONTPATH' ) ) { + $this->fontpath = FPDF_FONTPATH; + if ( substr( $this->fontpath, - 1 ) != '/' && substr( $this->fontpath, - 1 ) != '\\' ) { + $this->fontpath .= '/'; + } + } elseif ( is_dir( dirname( __FILE__ ) . '/font' ) ) { + $this->fontpath = dirname( __FILE__ ) . '/font/'; + } else { + $this->fontpath = ''; + } + // Core fonts + $this->CoreFonts = array( 'courier', 'helvetica', 'times', 'symbol', 'zapfdingbats' ); + // Scale factor + if ( $unit == 'pt' ) { + $this->k = 1; + } elseif ( $unit == 'mm' ) { + $this->k = 72 / 25.4; + } elseif ( $unit == 'cm' ) { + $this->k = 72 / 2.54; + } elseif ( $unit == 'in' ) { + $this->k = 72; + } else { + $this->Error( 'Incorrect unit: ' . $unit ); + } + // Page sizes + $this->StdPageSizes = array( + 'a3' => array( 841.89, 1190.55 ), + 'a4' => array( 595.28, 841.89 ), + 'a5' => array( 420.94, 595.28 ), + 'letter' => array( 612, 792 ), + 'legal' => array( 612, 1008 ) + ); + $size = $this->_getpagesize( $size ); + $this->DefPageSize = $size; + $this->CurPageSize = $size; + // Page orientation + $orientation = strtolower( $orientation ); + if ( $orientation == 'p' || $orientation == 'portrait' ) { + $this->DefOrientation = 'P'; + $this->w = $size[0]; + $this->h = $size[1]; + } elseif ( $orientation == 'l' || $orientation == 'landscape' ) { + $this->DefOrientation = 'L'; + $this->w = $size[1]; + $this->h = $size[0]; + } else { + $this->Error( 'Incorrect orientation: ' . $orientation ); + } + $this->CurOrientation = $this->DefOrientation; + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + // Page rotation + $this->CurRotation = 0; + // Page margins (1 cm) + $margin = 28.35 / $this->k; + $this->SetMargins( $margin, $margin ); + // Interior cell margin (1 mm) + $this->cMargin = $margin / 10; + // Line width (0.2 mm) + $this->LineWidth = .567 / $this->k; + // Automatic page break + $this->SetAutoPageBreak( true, 2 * $margin ); + // Default display mode + $this->SetDisplayMode( 'default' ); + // Enable compression + $this->SetCompression( true ); + // Set default PDF version number + $this->PDFVersion = '1.3'; + } + + /******************************************************************************* + * Protected methods * + *******************************************************************************/ + + protected function _dochecks() { + // Check mbstring overloading + if ( ini_get( 'mbstring.func_overload' ) & 2 ) { + $this->Error( 'mbstring overloading must be disabled' ); + } + // Ensure runtime magic quotes are disabled + if ( get_magic_quotes_runtime() ) { + @set_magic_quotes_runtime( 0 ); + } + } + + function Error( $msg ) { + // Fatal error + throw new Exception( 'FPDF error: ' . $msg ); + } + + protected function _getpagesize( $size ) { + if ( is_string( $size ) ) { + $size = strtolower( $size ); + if ( ! isset( $this->StdPageSizes[ $size ] ) ) { + $this->Error( 'Unknown page size: ' . $size ); + } + $a = $this->StdPageSizes[ $size ]; + + return array( $a[0] / $this->k, $a[1] / $this->k ); + } else { + if ( $size[0] > $size[1] ) { + return array( $size[1], $size[0] ); + } else { + return $size; + } + } + } + + function SetMargins( $left, $top, $right = null ) { + // Set left, top and right margins + $this->lMargin = $left; + $this->tMargin = $top; + if ( $right === null ) { + $right = $left; + } + $this->rMargin = $right; + } + + function SetAutoPageBreak( $auto, $margin = 0 ) { + // Set auto page break mode and triggering margin + $this->AutoPageBreak = $auto; + $this->bMargin = $margin; + $this->PageBreakTrigger = $this->h - $margin; + } + + function SetDisplayMode( $zoom, $layout = 'default' ) { + // Set display mode in viewer + if ( $zoom == 'fullpage' || $zoom == 'fullwidth' || $zoom == 'real' || $zoom == 'default' || ! is_string( $zoom ) ) { + $this->ZoomMode = $zoom; + } else { + $this->Error( 'Incorrect zoom display mode: ' . $zoom ); + } + if ( $layout == 'single' || $layout == 'continuous' || $layout == 'two' || $layout == 'default' ) { + $this->LayoutMode = $layout; + } else { + $this->Error( 'Incorrect layout display mode: ' . $layout ); + } + } + + function SetCompression( $compress ) { + // Set page compression + if ( function_exists( 'gzcompress' ) ) { + $this->compress = $compress; + } else { + $this->compress = false; + } + } + + function SetLeftMargin( $margin ) { + // Set left margin + $this->lMargin = $margin; + if ( $this->page > 0 && $this->x < $margin ) { + $this->x = $margin; + } + } + + function SetTopMargin( $margin ) { + // Set top margin + $this->tMargin = $margin; + } + + function SetRightMargin( $margin ) { + // Set right margin + $this->rMargin = $margin; + } + + function SetTitle( $title, $isUTF8 = false ) { + // Title of document + $this->metadata['Title'] = $isUTF8 ? $title : utf8_encode( $title ); + } + + function SetAuthor( $author, $isUTF8 = false ) { + // Author of document + $this->metadata['Author'] = $isUTF8 ? $author : utf8_encode( $author ); + } + + function SetSubject( $subject, $isUTF8 = false ) { + // Subject of document + $this->metadata['Subject'] = $isUTF8 ? $subject : utf8_encode( $subject ); + } + + function SetKeywords( $keywords, $isUTF8 = false ) { + // Keywords of document + $this->metadata['Keywords'] = $isUTF8 ? $keywords : utf8_encode( $keywords ); + } + + function SetCreator( $creator, $isUTF8 = false ) { + // Creator of document + $this->metadata['Creator'] = $isUTF8 ? $creator : utf8_encode( $creator ); + } + + function AliasNbPages( $alias = '{nb}' ) { + // Define an alias for total number of pages + $this->AliasNbPages = $alias; + } + + function PageNo() { + // Get current page number + return $this->page; + } + + function SetDrawColor( $r, $g = null, $b = null ) { + // Set color for all stroking operations + if ( ( $r == 0 && $g == 0 && $b == 0 ) || $g === null ) { + $this->DrawColor = sprintf( '%.3F G', $r / 255 ); + } else { + $this->DrawColor = sprintf( '%.3F %.3F %.3F RG', $r / 255, $g / 255, $b / 255 ); + } + if ( $this->page > 0 ) { + $this->_out( $this->DrawColor ); + } + } + + protected function _out( $s ) { + // Add a line to the document + if ( $this->state == 2 ) { + $this->pages[ $this->page ] .= $s . "\n"; + } elseif ( $this->state == 1 ) { + $this->_put( $s ); + } elseif ( $this->state == 0 ) { + $this->Error( 'No page has been added yet' ); + } elseif ( $this->state == 3 ) { + $this->Error( 'The document is closed' ); + } + } + + protected function _put( $s ) { + $this->buffer .= $s . "\n"; + } + + function SetFillColor( $r, $g = null, $b = null ) { + // Set color for all filling operations + if ( ( $r == 0 && $g == 0 && $b == 0 ) || $g === null ) { + $this->FillColor = sprintf( '%.3F g', $r / 255 ); + } else { + $this->FillColor = sprintf( '%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255 ); + } + $this->ColorFlag = ( $this->FillColor != $this->TextColor ); + if ( $this->page > 0 ) { + $this->_out( $this->FillColor ); + } + } + + function SetTextColor( $r, $g = null, $b = null ) { + // Set color for text + if ( ( $r == 0 && $g == 0 && $b == 0 ) || $g === null ) { + $this->TextColor = sprintf( '%.3F g', $r / 255 ); + } else { + $this->TextColor = sprintf( '%.3F %.3F %.3F rg', $r / 255, $g / 255, $b / 255 ); + } + $this->ColorFlag = ( $this->FillColor != $this->TextColor ); + } + + function SetLineWidth( $width ) { + // Set line width + $this->LineWidth = $width; + if ( $this->page > 0 ) { + $this->_out( sprintf( '%.2F w', $width * $this->k ) ); + } + } + + function Line( $x1, $y1, $x2, $y2 ) { + // Draw a line + $this->_out( sprintf( '%.2F %.2F m %.2F %.2F l S', $x1 * $this->k, ( $this->h - $y1 ) * $this->k, $x2 * $this->k, ( $this->h - $y2 ) * $this->k ) ); + } + + function Rect( $x, $y, $w, $h, $style = '' ) { + // Draw a rectangle + if ( $style == 'F' ) { + $op = 'f'; + } elseif ( $style == 'FD' || $style == 'DF' ) { + $op = 'B'; + } else { + $op = 'S'; + } + $this->_out( sprintf( '%.2F %.2F %.2F %.2F re %s', $x * $this->k, ( $this->h - $y ) * $this->k, $w * $this->k, - $h * $this->k, $op ) ); + } + + function SetFontSize( $size ) { + // Set font size in points + if ( $this->FontSizePt == $size ) { + return; + } + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + if ( $this->page > 0 ) { + $this->_out( sprintf( 'BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt ) ); + } + } + + function AddLink() { + // Create a new internal link + $n = count( $this->links ) + 1; + $this->links[ $n ] = array( 0, 0 ); + + return $n; + } + + function SetLink( $link, $y = 0, $page = - 1 ) { + // Set destination of internal link + if ( $y == - 1 ) { + $y = $this->y; + } + if ( $page == - 1 ) { + $page = $this->page; + } + $this->links[ $link ] = array( $page, $y ); + } + + function Text( $x, $y, $txt ) { + // Output a string + if ( ! isset( $this->CurrentFont ) ) { + $this->Error( 'No font has been set' ); + } + $s = sprintf( 'BT %.2F %.2F Td (%s) Tj ET', $x * $this->k, ( $this->h - $y ) * $this->k, $this->_escape( $txt ) ); + if ( $this->underline && $txt != '' ) { + $s .= ' ' . $this->_dounderline( $x, $y, $txt ); + } + if ( $this->ColorFlag ) { + $s = 'q ' . $this->TextColor . ' ' . $s . ' Q'; + } + $this->_out( $s ); + } + + protected function _escape( $s ) { + // Escape special characters + if ( strpos( $s, '(' ) !== false || strpos( $s, ')' ) !== false || strpos( $s, '\\' ) !== false || strpos( $s, "\r" ) !== false ) { + return str_replace( array( '\\', '(', ')', "\r" ), array( '\\\\', '\\(', '\\)', '\\r' ), $s ); + } else { + return $s; + } + } + + protected function _dounderline( $x, $y, $txt ) { + // Underline text + $up = $this->CurrentFont['up']; + $ut = $this->CurrentFont['ut']; + $w = $this->GetStringWidth( $txt ) + $this->ws * substr_count( $txt, ' ' ); + + return sprintf( '%.2F %.2F %.2F %.2F re f', $x * $this->k, ( $this->h - ( $y - $up / 1000 * $this->FontSize ) ) * $this->k, $w * $this->k, - $ut / 1000 * $this->FontSizePt ); + } + + function GetStringWidth( $s ) { + // Get width of a string in the current font + $s = (string) $s; + $cw = &$this->CurrentFont['cw']; + $w = 0; + $l = strlen( $s ); + for ( $i = 0; $i < $l; $i ++ ) { + $w += $cw[ $s[ $i ] ]; + } + + return $w * $this->FontSize / 1000; + } + + function MultiCell( $w, $h, $txt, $border = 0, $align = 'J', $fill = false ) { + // Output text with automatic or explicit line breaks + if ( ! isset( $this->CurrentFont ) ) { + $this->Error( 'No font has been set' ); + } + $cw = &$this->CurrentFont['cw']; + if ( $w == 0 ) { + $w = $this->w - $this->rMargin - $this->x; + } + $wmax = ( $w - 2 * $this->cMargin ) * 1000 / $this->FontSize; + $s = str_replace( "\r", '', $txt ); + $nb = strlen( $s ); + if ( $nb > 0 && $s[ $nb - 1 ] == "\n" ) { + $nb --; + } + $b = 0; + if ( $border ) { + if ( $border == 1 ) { + $border = 'LTRB'; + $b = 'LRT'; + $b2 = 'LR'; + } else { + $b2 = ''; + if ( strpos( $border, 'L' ) !== false ) { + $b2 .= 'L'; + } + if ( strpos( $border, 'R' ) !== false ) { + $b2 .= 'R'; + } + $b = ( strpos( $border, 'T' ) !== false ) ? $b2 . 'T' : $b2; + } + } + $sep = - 1; + $i = 0; + $j = 0; + $l = 0; + $ns = 0; + $nl = 1; + while ( $i < $nb ) { + // Get next character + $c = $s[ $i ]; + if ( $c == "\n" ) { + // Explicit line break + if ( $this->ws > 0 ) { + $this->ws = 0; + $this->_out( '0 Tw' ); + } + $this->Cell( $w, $h, substr( $s, $j, $i - $j ), $b, 2, $align, $fill ); + $i ++; + $sep = - 1; + $j = $i; + $l = 0; + $ns = 0; + $nl ++; + if ( $border && $nl == 2 ) { + $b = $b2; + } + continue; + } + if ( $c == ' ' ) { + $sep = $i; + $ls = $l; + $ns ++; + } + $l += $cw[ $c ]; + if ( $l > $wmax ) { + // Automatic line break + if ( $sep == - 1 ) { + if ( $i == $j ) { + $i ++; + } + if ( $this->ws > 0 ) { + $this->ws = 0; + $this->_out( '0 Tw' ); + } + $this->Cell( $w, $h, substr( $s, $j, $i - $j ), $b, 2, $align, $fill ); + } else { + if ( $align == 'J' ) { + $this->ws = ( $ns > 1 ) ? ( $wmax - $ls ) / 1000 * $this->FontSize / ( $ns - 1 ) : 0; + $this->_out( sprintf( '%.3F Tw', $this->ws * $this->k ) ); + } + $this->Cell( $w, $h, substr( $s, $j, $sep - $j ), $b, 2, $align, $fill ); + $i = $sep + 1; + } + $sep = - 1; + $j = $i; + $l = 0; + $ns = 0; + $nl ++; + if ( $border && $nl == 2 ) { + $b = $b2; + } + } else { + $i ++; + } + } + // Last chunk + if ( $this->ws > 0 ) { + $this->ws = 0; + $this->_out( '0 Tw' ); + } + if ( $border && strpos( $border, 'B' ) !== false ) { + $b .= 'B'; + } + $this->Cell( $w, $h, substr( $s, $j, $i - $j ), $b, 2, $align, $fill ); + $this->x = $this->lMargin; + } + + function Cell( $w, $h = 0, $txt = '', $border = 0, $ln = 0, $align = '', $fill = false, $link = '' ) { + // Output a cell + $k = $this->k; + if ( $this->y + $h > $this->PageBreakTrigger && ! $this->InHeader && ! $this->InFooter && $this->AcceptPageBreak() ) { + // Automatic page break + $x = $this->x; + $ws = $this->ws; + if ( $ws > 0 ) { + $this->ws = 0; + $this->_out( '0 Tw' ); + } + $this->AddPage( $this->CurOrientation, $this->CurPageSize, $this->CurRotation ); + $this->x = $x; + if ( $ws > 0 ) { + $this->ws = $ws; + $this->_out( sprintf( '%.3F Tw', $ws * $k ) ); + } + } + if ( $w == 0 ) { + $w = $this->w - $this->rMargin - $this->x; + } + $s = ''; + if ( $fill || $border == 1 ) { + if ( $fill ) { + $op = ( $border == 1 ) ? 'B' : 'f'; + } else { + $op = 'S'; + } + $s = sprintf( '%.2F %.2F %.2F %.2F re %s ', $this->x * $k, ( $this->h - $this->y ) * $k, $w * $k, - $h * $k, $op ); + } + if ( is_string( $border ) ) { + $x = $this->x; + $y = $this->y; + if ( strpos( $border, 'L' ) !== false ) { + $s .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $x * $k, ( $this->h - $y ) * $k, $x * $k, ( $this->h - ( $y + $h ) ) * $k ); + } + if ( strpos( $border, 'T' ) !== false ) { + $s .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $x * $k, ( $this->h - $y ) * $k, ( $x + $w ) * $k, ( $this->h - $y ) * $k ); + } + if ( strpos( $border, 'R' ) !== false ) { + $s .= sprintf( '%.2F %.2F m %.2F %.2F l S ', ( $x + $w ) * $k, ( $this->h - $y ) * $k, ( $x + $w ) * $k, ( $this->h - ( $y + $h ) ) * $k ); + } + if ( strpos( $border, 'B' ) !== false ) { + $s .= sprintf( '%.2F %.2F m %.2F %.2F l S ', $x * $k, ( $this->h - ( $y + $h ) ) * $k, ( $x + $w ) * $k, ( $this->h - ( $y + $h ) ) * $k ); + } + } + if ( $txt !== '' ) { + if ( ! isset( $this->CurrentFont ) ) { + $this->Error( 'No font has been set' ); + } + if ( $align == 'R' ) { + $dx = $w - $this->cMargin - $this->GetStringWidth( $txt ); + } elseif ( $align == 'C' ) { + $dx = ( $w - $this->GetStringWidth( $txt ) ) / 2; + } else { + $dx = $this->cMargin; + } + if ( $this->ColorFlag ) { + $s .= 'q ' . $this->TextColor . ' '; + } + $s .= sprintf( 'BT %.2F %.2F Td (%s) Tj ET', ( $this->x + $dx ) * $k, ( $this->h - ( $this->y + .5 * $h + .3 * $this->FontSize ) ) * $k, $this->_escape( $txt ) ); + if ( $this->underline ) { + $s .= ' ' . $this->_dounderline( $this->x + $dx, $this->y + .5 * $h + .3 * $this->FontSize, $txt ); + } + if ( $this->ColorFlag ) { + $s .= ' Q'; + } + if ( $link ) { + $this->Link( $this->x + $dx, $this->y + .5 * $h - .5 * $this->FontSize, $this->GetStringWidth( $txt ), $this->FontSize, $link ); + } + } + if ( $s ) { + $this->_out( $s ); + } + $this->lasth = $h; + if ( $ln > 0 ) { + // Go to next line + $this->y += $h; + if ( $ln == 1 ) { + $this->x = $this->lMargin; + } + } else { + $this->x += $w; + } + } + + function AcceptPageBreak() { + // Accept automatic page break or not + return $this->AutoPageBreak; + } + + function AddPage( $orientation = '', $size = '', $rotation = 0 ) { + // Start a new page + if ( $this->state == 3 ) { + $this->Error( 'The document is closed' ); + } + $family = $this->FontFamily; + $style = $this->FontStyle . ( $this->underline ? 'U' : '' ); + $fontsize = $this->FontSizePt; + $lw = $this->LineWidth; + $dc = $this->DrawColor; + $fc = $this->FillColor; + $tc = $this->TextColor; + $cf = $this->ColorFlag; + if ( $this->page > 0 ) { + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + } + // Start new page + $this->_beginpage( $orientation, $size, $rotation ); + // Set line cap style to square + $this->_out( '2 J' ); + // Set line width + $this->LineWidth = $lw; + $this->_out( sprintf( '%.2F w', $lw * $this->k ) ); + // Set font + if ( $family ) { + $this->SetFont( $family, $style, $fontsize ); + } + // Set colors + $this->DrawColor = $dc; + if ( $dc != '0 G' ) { + $this->_out( $dc ); + } + $this->FillColor = $fc; + if ( $fc != '0 g' ) { + $this->_out( $fc ); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; + // Page header + $this->InHeader = true; + $this->Header(); + $this->InHeader = false; + // Restore line width + if ( $this->LineWidth != $lw ) { + $this->LineWidth = $lw; + $this->_out( sprintf( '%.2F w', $lw * $this->k ) ); + } + // Restore font + if ( $family ) { + $this->SetFont( $family, $style, $fontsize ); + } + // Restore colors + if ( $this->DrawColor != $dc ) { + $this->DrawColor = $dc; + $this->_out( $dc ); + } + if ( $this->FillColor != $fc ) { + $this->FillColor = $fc; + $this->_out( $fc ); + } + $this->TextColor = $tc; + $this->ColorFlag = $cf; + } + + function Footer() { + // To be implemented in your own inherited class + } + + protected function _endpage() { + $this->state = 1; + } + + protected function _beginpage( $orientation, $size, $rotation ) { + $this->page ++; + $this->pages[ $this->page ] = ''; + $this->state = 2; + $this->x = $this->lMargin; + $this->y = $this->tMargin; + $this->FontFamily = ''; + // Check page size and orientation + if ( $orientation == '' ) { + $orientation = $this->DefOrientation; + } else { + $orientation = strtoupper( $orientation[0] ); + } + if ( $size == '' ) { + $size = $this->DefPageSize; + } else { + $size = $this->_getpagesize( $size ); + } + if ( $orientation != $this->CurOrientation || $size[0] != $this->CurPageSize[0] || $size[1] != $this->CurPageSize[1] ) { + // New size or orientation + if ( $orientation == 'P' ) { + $this->w = $size[0]; + $this->h = $size[1]; + } else { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + } + if ( $orientation != $this->DefOrientation || $size[0] != $this->DefPageSize[0] || $size[1] != $this->DefPageSize[1] ) { + $this->PageInfo[ $this->page ]['size'] = array( $this->wPt, $this->hPt ); + } + if ( $rotation != 0 ) { + if ( $rotation % 90 != 0 ) { + $this->Error( 'Incorrect rotation value: ' . $rotation ); + } + $this->CurRotation = $rotation; + $this->PageInfo[ $this->page ]['rotation'] = $rotation; + } + } + + function SetFont( $family, $style = '', $size = 0 ) { + // Select a font; size given in points + if ( $family == '' ) { + $family = $this->FontFamily; + } else { + $family = strtolower( $family ); + } + $style = strtoupper( $style ); + if ( strpos( $style, 'U' ) !== false ) { + $this->underline = true; + $style = str_replace( 'U', '', $style ); + } else { + $this->underline = false; + } + if ( $style == 'IB' ) { + $style = 'BI'; + } + if ( $size == 0 ) { + $size = $this->FontSizePt; + } + // Test if font is already selected + if ( $this->FontFamily == $family && $this->FontStyle == $style && $this->FontSizePt == $size ) { + return; + } + // Test if font is already loaded + $fontkey = $family . $style; + if ( ! isset( $this->fonts[ $fontkey ] ) ) { + // Test if one of the core fonts + if ( $family == 'arial' ) { + $family = 'helvetica'; + } + if ( in_array( $family, $this->CoreFonts ) ) { + if ( $family == 'symbol' || $family == 'zapfdingbats' ) { + $style = ''; + } + $fontkey = $family . $style; + if ( ! isset( $this->fonts[ $fontkey ] ) ) { + $this->AddFont( $family, $style ); + } + } else { + $this->Error( 'Undefined font: ' . $family . ' ' . $style ); + } + } + // Select it + $this->FontFamily = $family; + $this->FontStyle = $style; + $this->FontSizePt = $size; + $this->FontSize = $size / $this->k; + $this->CurrentFont = &$this->fonts[ $fontkey ]; + if ( $this->page > 0 ) { + $this->_out( sprintf( 'BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt ) ); + } + } + + function AddFont( $family, $style = '', $file = '' ) { + // Add a TrueType, OpenType or Type1 font + $family = strtolower( $family ); + if ( $file == '' ) { + $file = str_replace( ' ', '', $family ) . strtolower( $style ) . '.php'; + } + $style = strtoupper( $style ); + if ( $style == 'IB' ) { + $style = 'BI'; + } + $fontkey = $family . $style; + if ( isset( $this->fonts[ $fontkey ] ) ) { + return; + } + $info = $this->_loadfont( $file ); + $info['i'] = count( $this->fonts ) + 1; + if ( ! empty( $info['file'] ) ) { + // Embedded font + if ( $info['type'] == 'TrueType' ) { + $this->FontFiles[ $info['file'] ] = array( 'length1' => $info['originalsize'] ); + } else { + $this->FontFiles[ $info['file'] ] = array( 'length1' => $info['size1'], 'length2' => $info['size2'] ); + } + } + $this->fonts[ $fontkey ] = $info; + } + + protected function _loadfont( $font ) { + // Load a font definition file from the font directory + if ( strpos( $font, '/' ) !== false || strpos( $font, "\\" ) !== false ) { + $this->Error( 'Incorrect font definition file name: ' . $font ); + } + include( $this->fontpath . $font ); + if ( ! isset( $name ) ) { + $this->Error( 'Could not include font definition file' ); + } + if ( isset( $enc ) ) { + $enc = strtolower( $enc ); + } + if ( ! isset( $subsetted ) ) { + $subsetted = false; + } + + return get_defined_vars(); + } + + function Header() { + // To be implemented in your own inherited class + } + + function Link( $x, $y, $w, $h, $link ) { + // Put a link on the page + $this->PageLinks[ $this->page ][] = array( + $x * $this->k, + $this->hPt - $y * $this->k, + $w * $this->k, + $h * $this->k, + $link + ); + } + + function Write( $h, $txt, $link = '' ) { + // Output text in flowing mode + if ( ! isset( $this->CurrentFont ) ) { + $this->Error( 'No font has been set' ); + } + $cw = &$this->CurrentFont['cw']; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ( $w - 2 * $this->cMargin ) * 1000 / $this->FontSize; + $s = str_replace( "\r", '', $txt ); + $nb = strlen( $s ); + $sep = - 1; + $i = 0; + $j = 0; + $l = 0; + $nl = 1; + while ( $i < $nb ) { + // Get next character + $c = $s[ $i ]; + if ( $c == "\n" ) { + // Explicit line break + $this->Cell( $w, $h, substr( $s, $j, $i - $j ), 0, 2, '', false, $link ); + $i ++; + $sep = - 1; + $j = $i; + $l = 0; + if ( $nl == 1 ) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ( $w - 2 * $this->cMargin ) * 1000 / $this->FontSize; + } + $nl ++; + continue; + } + if ( $c == ' ' ) { + $sep = $i; + } + $l += $cw[ $c ]; + if ( $l > $wmax ) { + // Automatic line break + if ( $sep == - 1 ) { + if ( $this->x > $this->lMargin ) { + // Move to next line + $this->x = $this->lMargin; + $this->y += $h; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ( $w - 2 * $this->cMargin ) * 1000 / $this->FontSize; + $i ++; + $nl ++; + continue; + } + if ( $i == $j ) { + $i ++; + } + $this->Cell( $w, $h, substr( $s, $j, $i - $j ), 0, 2, '', false, $link ); + } else { + $this->Cell( $w, $h, substr( $s, $j, $sep - $j ), 0, 2, '', false, $link ); + $i = $sep + 1; + } + $sep = - 1; + $j = $i; + $l = 0; + if ( $nl == 1 ) { + $this->x = $this->lMargin; + $w = $this->w - $this->rMargin - $this->x; + $wmax = ( $w - 2 * $this->cMargin ) * 1000 / $this->FontSize; + } + $nl ++; + } else { + $i ++; + } + } + // Last chunk + if ( $i != $j ) { + $this->Cell( $l / 1000 * $this->FontSize, $h, substr( $s, $j ), 0, 0, '', false, $link ); + } + } + + function Ln( $h = null ) { + // Line feed; default value is the last cell height + $this->x = $this->lMargin; + if ( $h === null ) { + $this->y += $this->lasth; + } else { + $this->y += $h; + } + } + + function Image( $file, $x = null, $y = null, $w = 0, $h = 0, $type = '', $link = '' ) { + // Put an image on the page + if ( $file == '' ) { + $this->Error( 'Image file name is empty' ); + } + if ( ! isset( $this->images[ $file ] ) ) { + // First use of this image, get info + if ( $type == '' ) { + $pos = strrpos( $file, '.' ); + if ( ! $pos ) { + $this->Error( 'Image file has no extension and no type was specified: ' . $file ); + } + $type = substr( $file, $pos + 1 ); + } + $type = strtolower( $type ); + if ( $type == 'jpeg' ) { + $type = 'jpg'; + } + $mtd = '_parse' . $type; + if ( ! method_exists( $this, $mtd ) ) { + $this->Error( 'Unsupported image type: ' . $type ); + } + $info = $this->$mtd( $file ); + $info['i'] = count( $this->images ) + 1; + $this->images[ $file ] = $info; + } else { + $info = $this->images[ $file ]; + } + + // Automatic width and height calculation if needed + if ( $w == 0 && $h == 0 ) { + // Put image at 96 dpi + $w = - 96; + $h = - 96; + } + if ( $w < 0 ) { + $w = - $info['w'] * 72 / $w / $this->k; + } + if ( $h < 0 ) { + $h = - $info['h'] * 72 / $h / $this->k; + } + if ( $w == 0 ) { + $w = $h * $info['w'] / $info['h']; + } + if ( $h == 0 ) { + $h = $w * $info['h'] / $info['w']; + } + + // Flowing mode + if ( $y === null ) { + if ( $this->y + $h > $this->PageBreakTrigger && ! $this->InHeader && ! $this->InFooter && $this->AcceptPageBreak() ) { + // Automatic page break + $x2 = $this->x; + $this->AddPage( $this->CurOrientation, $this->CurPageSize, $this->CurRotation ); + $this->x = $x2; + } + $y = $this->y; + $this->y += $h; + } + + if ( $x === null ) { + $x = $this->x; + } + $this->_out( sprintf( 'q %.2F 0 0 %.2F %.2F %.2F cm /I%d Do Q', $w * $this->k, $h * $this->k, $x * $this->k, ( $this->h - ( $y + $h ) ) * $this->k, $info['i'] ) ); + if ( $link ) { + $this->Link( $x, $y, $w, $h, $link ); + } + } + + function GetPageWidth() { + // Get current page width + return $this->w; + } + + function GetPageHeight() { + // Get current page height + return $this->h; + } + + function GetX() { + // Get x position + return $this->x; + } + + function SetX( $x ) { + // Set x position + if ( $x >= 0 ) { + $this->x = $x; + } else { + $this->x = $this->w + $x; + } + } + + function GetY() { + // Get y position + return $this->y; + } + + function SetXY( $x, $y ) { + // Set x and y positions + $this->SetX( $x ); + $this->SetY( $y, false ); + } + + function SetY( $y, $resetX = true ) { + // Set y position and optionally reset x + if ( $y >= 0 ) { + $this->y = $y; + } else { + $this->y = $this->h + $y; + } + if ( $resetX ) { + $this->x = $this->lMargin; + } + } + + function Output( $dest = '', $name = '', $isUTF8 = false ) { + // Output PDF to some destination + $this->Close(); + if ( strlen( $name ) == 1 && strlen( $dest ) != 1 ) { + // Fix parameter order + $tmp = $dest; + $dest = $name; + $name = $tmp; + } + if ( $dest == '' ) { + $dest = 'I'; + } + if ( $name == '' ) { + $name = 'doc.pdf'; + } + switch ( strtoupper( $dest ) ) { + case 'I': + // Send to standard output + $this->_checkoutput(); + if ( PHP_SAPI != 'cli' ) { + // We send to a browser + header( 'Content-Type: application/pdf' ); + header( 'Content-Disposition: inline; ' . $this->_httpencode( 'filename', $name, $isUTF8 ) ); + header( 'Cache-Control: private, max-age=0, must-revalidate' ); + header( 'Pragma: public' ); + } + echo $this->buffer; + break; + case 'D': + // Download file + $this->_checkoutput(); + header( 'Content-Type: application/x-download' ); + header( 'Content-Disposition: attachment; ' . $this->_httpencode( 'filename', $name, $isUTF8 ) ); + header( 'Cache-Control: private, max-age=0, must-revalidate' ); + header( 'Pragma: public' ); + echo $this->buffer; + break; + case 'F': + // Save to local file + if ( ! file_put_contents( $name, $this->buffer ) ) { + $this->Error( 'Unable to create output file: ' . $name ); + } + break; + case 'S': + // Return as a string + return $this->buffer; + default: + $this->Error( 'Incorrect output destination: ' . $dest ); + } + + return ''; + } + + function Close() { + // Terminate document + if ( $this->state == 3 ) { + return; + } + if ( $this->page == 0 ) { + $this->AddPage(); + } + // Page footer + $this->InFooter = true; + $this->Footer(); + $this->InFooter = false; + // Close page + $this->_endpage(); + // Close document + $this->_enddoc(); + } + + protected function _enddoc() { + $this->_putheader(); + $this->_putpages(); + $this->_putresources(); + // Info + $this->_newobj(); + $this->_put( '<<' ); + $this->_putinfo(); + $this->_put( '>>' ); + $this->_put( 'endobj' ); + // Catalog + $this->_newobj(); + $this->_put( '<<' ); + $this->_putcatalog(); + $this->_put( '>>' ); + $this->_put( 'endobj' ); + // Cross-ref + $offset = $this->_getoffset(); + $this->_put( 'xref' ); + $this->_put( '0 ' . ( $this->n + 1 ) ); + $this->_put( '0000000000 65535 f ' ); + for ( $i = 1; $i <= $this->n; $i ++ ) { + $this->_put( sprintf( '%010d 00000 n ', $this->offsets[ $i ] ) ); + } + // Trailer + $this->_put( 'trailer' ); + $this->_put( '<<' ); + $this->_puttrailer(); + $this->_put( '>>' ); + $this->_put( 'startxref' ); + $this->_put( $offset ); + $this->_put( '%%EOF' ); + $this->state = 3; + } + + protected function _putheader() { + $this->_put( '%PDF-' . $this->PDFVersion ); + } + + protected function _putpages() { + $nb = $this->page; + for ( $n = 1; $n <= $nb; $n ++ ) { + $this->PageInfo[ $n ]['n'] = $this->n + 1 + 2 * ( $n - 1 ); + } + for ( $n = 1; $n <= $nb; $n ++ ) { + $this->_putpage( $n ); + } + // Pages root + $this->_newobj( 1 ); + $this->_put( '<PageInfo[ $n ]['n'] . ' 0 R '; + } + $this->_put( $kids . ']' ); + $this->_put( '/Count ' . $nb ); + if ( $this->DefOrientation == 'P' ) { + $w = $this->DefPageSize[0]; + $h = $this->DefPageSize[1]; + } else { + $w = $this->DefPageSize[1]; + $h = $this->DefPageSize[0]; + } + $this->_put( sprintf( '/MediaBox [0 0 %.2F %.2F]', $w * $this->k, $h * $this->k ) ); + $this->_put( '>>' ); + $this->_put( 'endobj' ); + } + + protected function _putpage( $n ) { + $this->_newobj(); + $this->_put( '<_put( '/Parent 1 0 R' ); + if ( isset( $this->PageInfo[ $n ]['size'] ) ) { + $this->_put( sprintf( '/MediaBox [0 0 %.2F %.2F]', $this->PageInfo[ $n ]['size'][0], $this->PageInfo[ $n ]['size'][1] ) ); + } + if ( isset( $this->PageInfo[ $n ]['rotation'] ) ) { + $this->_put( '/Rotate ' . $this->PageInfo[ $n ]['rotation'] ); + } + $this->_put( '/Resources 2 0 R' ); + if ( isset( $this->PageLinks[ $n ] ) ) { + // Links + $annots = '/Annots ['; + foreach ( $this->PageLinks[ $n ] as $pl ) { + $rect = sprintf( '%.2F %.2F %.2F %.2F', $pl[0], $pl[1], $pl[0] + $pl[2], $pl[1] - $pl[3] ); + $annots .= '<_textstring( $pl[4] ) . '>>>>'; + } else { + $l = $this->links[ $pl[4] ]; + if ( isset( $this->PageInfo[ $l[0] ]['size'] ) ) { + $h = $this->PageInfo[ $l[0] ]['size'][1]; + } else { + $h = ( $this->DefOrientation == 'P' ) ? $this->DefPageSize[1] * $this->k : $this->DefPageSize[0] * $this->k; + } + $annots .= sprintf( '/Dest [%d 0 R /XYZ 0 %.2F null]>>', $this->PageInfo[ $l[0] ]['n'], $h - $l[1] * $this->k ); + } + } + $this->_put( $annots . ']' ); + } + if ( $this->WithAlpha ) { + $this->_put( '/Group <>' ); + } + $this->_put( '/Contents ' . ( $this->n + 1 ) . ' 0 R>>' ); + $this->_put( 'endobj' ); + // Page content + if ( ! empty( $this->AliasNbPages ) ) { + $this->pages[ $n ] = str_replace( $this->AliasNbPages, $this->page, $this->pages[ $n ] ); + } + $this->_putstreamobject( $this->pages[ $n ] ); + } + + protected function _newobj( $n = null ) { + // Begin a new object + if ( $n === null ) { + $n = ++ $this->n; + } + $this->offsets[ $n ] = $this->_getoffset(); + $this->_put( $n . ' 0 obj' ); + } + + protected function _getoffset() { + return strlen( $this->buffer ); + } + + protected function _textstring( $s ) { + // Format a text string + if ( ! $this->_isascii( $s ) ) { + $s = $this->_UTF8toUTF16( $s ); + } + + return '(' . $this->_escape( $s ) . ')'; + } + + protected function _isascii( $s ) { + // Test if string is ASCII + $nb = strlen( $s ); + for ( $i = 0; $i < $nb; $i ++ ) { + if ( ord( $s[ $i ] ) > 127 ) { + return false; + } + } + + return true; + } + + protected function _UTF8toUTF16( $s ) { + // Convert UTF-8 to UTF-16BE with BOM + $res = "\xFE\xFF"; + $nb = strlen( $s ); + $i = 0; + while ( $i < $nb ) { + $c1 = ord( $s[ $i ++ ] ); + if ( $c1 >= 224 ) { + // 3-byte character + $c2 = ord( $s[ $i ++ ] ); + $c3 = ord( $s[ $i ++ ] ); + $res .= chr( ( ( $c1 & 0x0F ) << 4 ) + ( ( $c2 & 0x3C ) >> 2 ) ); + $res .= chr( ( ( $c2 & 0x03 ) << 6 ) + ( $c3 & 0x3F ) ); + } elseif ( $c1 >= 192 ) { + // 2-byte character + $c2 = ord( $s[ $i ++ ] ); + $res .= chr( ( $c1 & 0x1C ) >> 2 ); + $res .= chr( ( ( $c1 & 0x03 ) << 6 ) + ( $c2 & 0x3F ) ); + } else { + // Single-byte character + $res .= "\0" . chr( $c1 ); + } + } + + return $res; + } + + protected function _putstreamobject( $data ) { + if ( $this->compress ) { + $entries = '/Filter /FlateDecode '; + $data = gzcompress( $data ); + } else { + $entries = ''; + } + $entries .= '/Length ' . strlen( $data ); + $this->_newobj(); + $this->_put( '<<' . $entries . '>>' ); + $this->_putstream( $data ); + $this->_put( 'endobj' ); + } + + protected function _putstream( $data ) { + $this->_put( 'stream' ); + $this->_put( $data ); + $this->_put( 'endstream' ); + } + + protected function _putresources() { + $this->_putfonts(); + $this->_putimages(); + // Resource dictionary + $this->_newobj( 2 ); + $this->_put( '<<' ); + $this->_putresourcedict(); + $this->_put( '>>' ); + $this->_put( 'endobj' ); + } + + protected function _putfonts() { + foreach ( $this->FontFiles as $file => $info ) { + // Font file embedding + $this->_newobj(); + $this->FontFiles[ $file ]['n'] = $this->n; + $font = file_get_contents( $this->fontpath . $file, true ); + if ( ! $font ) { + $this->Error( 'Font file not found: ' . $file ); + } + $compressed = ( substr( $file, - 2 ) == '.z' ); + if ( ! $compressed && isset( $info['length2'] ) ) { + $font = substr( $font, 6, $info['length1'] ) . substr( $font, 6 + $info['length1'] + 6, $info['length2'] ); + } + $this->_put( '<_put( '/Filter /FlateDecode' ); + } + $this->_put( '/Length1 ' . $info['length1'] ); + if ( isset( $info['length2'] ) ) { + $this->_put( '/Length2 ' . $info['length2'] . ' /Length3 0' ); + } + $this->_put( '>>' ); + $this->_putstream( $font ); + $this->_put( 'endobj' ); + } + foreach ( $this->fonts as $k => $font ) { + // Encoding + if ( isset( $font['diff'] ) ) { + if ( ! isset( $this->encodings[ $font['enc'] ] ) ) { + $this->_newobj(); + $this->_put( '<>' ); + $this->_put( 'endobj' ); + $this->encodings[ $font['enc'] ] = $this->n; + } + } + // ToUnicode CMap + if ( isset( $font['uv'] ) ) { + if ( isset( $font['enc'] ) ) { + $cmapkey = $font['enc']; + } else { + $cmapkey = $font['name']; + } + if ( ! isset( $this->cmaps[ $cmapkey ] ) ) { + $cmap = $this->_tounicodecmap( $font['uv'] ); + $this->_putstreamobject( $cmap ); + $this->cmaps[ $cmapkey ] = $this->n; + } + } + // Font object + $this->fonts[ $k ]['n'] = $this->n + 1; + $type = $font['type']; + $name = $font['name']; + if ( $font['subsetted'] ) { + $name = 'AAAAAA+' . $name; + } + if ( $type == 'Core' ) { + // Core font + $this->_newobj(); + $this->_put( '<_put( '/BaseFont /' . $name ); + $this->_put( '/Subtype /Type1' ); + if ( $name != 'Symbol' && $name != 'ZapfDingbats' ) { + $this->_put( '/Encoding /WinAnsiEncoding' ); + } + if ( isset( $font['uv'] ) ) { + $this->_put( '/ToUnicode ' . $this->cmaps[ $cmapkey ] . ' 0 R' ); + } + $this->_put( '>>' ); + $this->_put( 'endobj' ); + } elseif ( $type == 'Type1' || $type == 'TrueType' ) { + // Additional Type1 or TrueType/OpenType font + $this->_newobj(); + $this->_put( '<_put( '/BaseFont /' . $name ); + $this->_put( '/Subtype /' . $type ); + $this->_put( '/FirstChar 32 /LastChar 255' ); + $this->_put( '/Widths ' . ( $this->n + 1 ) . ' 0 R' ); + $this->_put( '/FontDescriptor ' . ( $this->n + 2 ) . ' 0 R' ); + if ( isset( $font['diff'] ) ) { + $this->_put( '/Encoding ' . $this->encodings[ $font['enc'] ] . ' 0 R' ); + } else { + $this->_put( '/Encoding /WinAnsiEncoding' ); + } + if ( isset( $font['uv'] ) ) { + $this->_put( '/ToUnicode ' . $this->cmaps[ $cmapkey ] . ' 0 R' ); + } + $this->_put( '>>' ); + $this->_put( 'endobj' ); + // Widths + $this->_newobj(); + $cw = &$font['cw']; + $s = '['; + for ( $i = 32; $i <= 255; $i ++ ) { + $s .= $cw[ chr( $i ) ] . ' '; + } + $this->_put( $s . ']' ); + $this->_put( 'endobj' ); + // Descriptor + $this->_newobj(); + $s = '< $v ) { + $s .= ' /' . $k . ' ' . $v; + } + if ( ! empty( $font['file'] ) ) { + $s .= ' /FontFile' . ( $type == 'Type1' ? '' : '2' ) . ' ' . $this->FontFiles[ $font['file'] ]['n'] . ' 0 R'; + } + $this->_put( $s . '>>' ); + $this->_put( 'endobj' ); + } else { + // Allow for additional types + $mtd = '_put' . strtolower( $type ); + if ( ! method_exists( $this, $mtd ) ) { + $this->Error( 'Unsupported font type: ' . $type ); + } + $this->$mtd( $font ); + } + } + } + + protected function _tounicodecmap( $uv ) { + $ranges = ''; + $nbr = 0; + $chars = ''; + $nbc = 0; + foreach ( $uv as $c => $v ) { + if ( is_array( $v ) ) { + $ranges .= sprintf( "<%02X> <%02X> <%04X>\n", $c, $c + $v[1] - 1, $v[0] ); + $nbr ++; + } else { + $chars .= sprintf( "<%02X> <%04X>\n", $c, $v ); + $nbc ++; + } + } + $s = "/CIDInit /ProcSet findresource begin\n"; + $s .= "12 dict begin\n"; + $s .= "begincmap\n"; + $s .= "/CIDSystemInfo\n"; + $s .= "< 0 ) { + $s .= "$nbr beginbfrange\n"; + $s .= $ranges; + $s .= "endbfrange\n"; + } + if ( $nbc > 0 ) { + $s .= "$nbc beginbfchar\n"; + $s .= $chars; + $s .= "endbfchar\n"; + } + $s .= "endcmap\n"; + $s .= "CMapName currentdict /CMap defineresource pop\n"; + $s .= "end\n"; + $s .= "end"; + + return $s; + } + + protected function _putimages() { + foreach ( array_keys( $this->images ) as $file ) { + $this->_putimage( $this->images[ $file ] ); + unset( $this->images[ $file ]['data'] ); + unset( $this->images[ $file ]['smask'] ); + } + } + + protected function _putimage( &$info ) { + $this->_newobj(); + $info['n'] = $this->n; + $this->_put( '<_put( '/Subtype /Image' ); + $this->_put( '/Width ' . $info['w'] ); + $this->_put( '/Height ' . $info['h'] ); + if ( $info['cs'] == 'Indexed' ) { + $this->_put( '/ColorSpace [/Indexed /DeviceRGB ' . ( strlen( $info['pal'] ) / 3 - 1 ) . ' ' . ( $this->n + 1 ) . ' 0 R]' ); + } else { + $this->_put( '/ColorSpace /' . $info['cs'] ); + if ( $info['cs'] == 'DeviceCMYK' ) { + $this->_put( '/Decode [1 0 1 0 1 0 1 0]' ); + } + } + $this->_put( '/BitsPerComponent ' . $info['bpc'] ); + if ( isset( $info['f'] ) ) { + $this->_put( '/Filter /' . $info['f'] ); + } + if ( isset( $info['dp'] ) ) { + $this->_put( '/DecodeParms <<' . $info['dp'] . '>>' ); + } + if ( isset( $info['trns'] ) && is_array( $info['trns'] ) ) { + $trns = ''; + for ( $i = 0; $i < count( $info['trns'] ); $i ++ ) { + $trns .= $info['trns'][ $i ] . ' ' . $info['trns'][ $i ] . ' '; + } + $this->_put( '/Mask [' . $trns . ']' ); + } + if ( isset( $info['smask'] ) ) { + $this->_put( '/SMask ' . ( $this->n + 1 ) . ' 0 R' ); + } + $this->_put( '/Length ' . strlen( $info['data'] ) . '>>' ); + $this->_putstream( $info['data'] ); + $this->_put( 'endobj' ); + // Soft mask + if ( isset( $info['smask'] ) ) { + $dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' . $info['w']; + $smask = array( + 'w' => $info['w'], + 'h' => $info['h'], + 'cs' => 'DeviceGray', + 'bpc' => 8, + 'f' => $info['f'], + 'dp' => $dp, + 'data' => $info['smask'] + ); + $this->_putimage( $smask ); + } + // Palette + if ( $info['cs'] == 'Indexed' ) { + $this->_putstreamobject( $info['pal'] ); + } + } + + protected function _putresourcedict() { + $this->_put( '/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]' ); + $this->_put( '/Font <<' ); + foreach ( $this->fonts as $font ) { + $this->_put( '/F' . $font['i'] . ' ' . $font['n'] . ' 0 R' ); + } + $this->_put( '>>' ); + $this->_put( '/XObject <<' ); + $this->_putxobjectdict(); + $this->_put( '>>' ); + } + + protected function _putxobjectdict() { + foreach ( $this->images as $image ) { + $this->_put( '/I' . $image['i'] . ' ' . $image['n'] . ' 0 R' ); + } + } + + protected function _putinfo() { + $this->metadata['Producer'] = 'FPDF ' . FPDF_VERSION; + $this->metadata['CreationDate'] = 'D:' . @date( 'YmdHis' ); + foreach ( $this->metadata as $key => $value ) { + $this->_put( '/' . $key . ' ' . $this->_textstring( $value ) ); + } + } + + protected function _putcatalog() { + $n = $this->PageInfo[1]['n']; + $this->_put( '/Type /Catalog' ); + $this->_put( '/Pages 1 0 R' ); + if ( $this->ZoomMode == 'fullpage' ) { + $this->_put( '/OpenAction [' . $n . ' 0 R /Fit]' ); + } elseif ( $this->ZoomMode == 'fullwidth' ) { + $this->_put( '/OpenAction [' . $n . ' 0 R /FitH null]' ); + } elseif ( $this->ZoomMode == 'real' ) { + $this->_put( '/OpenAction [' . $n . ' 0 R /XYZ null null 1]' ); + } elseif ( ! is_string( $this->ZoomMode ) ) { + $this->_put( '/OpenAction [' . $n . ' 0 R /XYZ null null ' . sprintf( '%.2F', $this->ZoomMode / 100 ) . ']' ); + } + if ( $this->LayoutMode == 'single' ) { + $this->_put( '/PageLayout /SinglePage' ); + } elseif ( $this->LayoutMode == 'continuous' ) { + $this->_put( '/PageLayout /OneColumn' ); + } elseif ( $this->LayoutMode == 'two' ) { + $this->_put( '/PageLayout /TwoColumnLeft' ); + } + } + + protected function _puttrailer() { + $this->_put( '/Size ' . ( $this->n + 1 ) ); + $this->_put( '/Root ' . $this->n . ' 0 R' ); + $this->_put( '/Info ' . ( $this->n - 1 ) . ' 0 R' ); + } + + protected function _checkoutput() { + if ( PHP_SAPI != 'cli' ) { + if ( headers_sent( $file, $line ) ) { + $this->Error( "Some data has already been output, can't send PDF file (output started at $file:$line)" ); + } + } + if ( ob_get_length() ) { + // The output buffer is not empty + if ( preg_match( '/^(\xEF\xBB\xBF)?\s*$/', ob_get_contents() ) ) { + // It contains only a UTF-8 BOM and/or whitespace, let's clean it + ob_clean(); + } +// else +// $this->Error("Some data has already been output, can't send PDF file"); + } + } + + protected function _httpencode( $param, $value, $isUTF8 ) { + // Encode HTTP header field parameter + if ( $this->_isascii( $value ) ) { + return $param . '="' . $value . '"'; + } + if ( ! $isUTF8 ) { + $value = utf8_encode( $value ); + } + if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE' ) !== false ) { + return $param . '="' . rawurlencode( $value ) . '"'; + } else { + return $param . "*=UTF-8''" . rawurlencode( $value ); + } + } + + protected function _parsejpg( $file ) { + // Extract info from a JPEG file + $a = getimagesize( $file ); + if ( ! $a ) { + $this->Error( 'Missing or incorrect image file: ' . $file ); + } + if ( $a[2] != 2 ) { + $this->Error( 'Not a JPEG file: ' . $file ); + } + if ( ! isset( $a['channels'] ) || $a['channels'] == 3 ) { + $colspace = 'DeviceRGB'; + } elseif ( $a['channels'] == 4 ) { + $colspace = 'DeviceCMYK'; + } else { + $colspace = 'DeviceGray'; + } + $bpc = isset( $a['bits'] ) ? $a['bits'] : 8; + $data = file_get_contents( $file ); + + return array( + 'w' => $a[0], + 'h' => $a[1], + 'cs' => $colspace, + 'bpc' => $bpc, + 'f' => 'DCTDecode', + 'data' => $data + ); + } + + protected function _parsepng( $file ) { + // Extract info from a PNG file + $f = fopen( $file, 'rb' ); + if ( ! $f ) { + $this->Error( 'Can\'t open image file: ' . $file ); + } + $info = $this->_parsepngstream( $f, $file ); + fclose( $f ); + + return $info; + } + + protected function _parsepngstream( $f, $file ) { + // Check signature + if ( $this->_readstream( $f, 8 ) != chr( 137 ) . 'PNG' . chr( 13 ) . chr( 10 ) . chr( 26 ) . chr( 10 ) ) { + $this->Error( 'Not a PNG file: ' . $file ); + } + + // Read header chunk + $this->_readstream( $f, 4 ); + if ( $this->_readstream( $f, 4 ) != 'IHDR' ) { + $this->Error( 'Incorrect PNG file: ' . $file ); + } + $w = $this->_readint( $f ); + $h = $this->_readint( $f ); + $bpc = ord( $this->_readstream( $f, 1 ) ); + if ( $bpc > 8 ) { + $this->Error( '16-bit depth not supported: ' . $file ); + } + $ct = ord( $this->_readstream( $f, 1 ) ); + if ( $ct == 0 || $ct == 4 ) { + $colspace = 'DeviceGray'; + } elseif ( $ct == 2 || $ct == 6 ) { + $colspace = 'DeviceRGB'; + } elseif ( $ct == 3 ) { + $colspace = 'Indexed'; + } else { + $this->Error( 'Unknown color type: ' . $file ); + } + if ( ord( $this->_readstream( $f, 1 ) ) != 0 ) { + $this->Error( 'Unknown compression method: ' . $file ); + } + if ( ord( $this->_readstream( $f, 1 ) ) != 0 ) { + $this->Error( 'Unknown filter method: ' . $file ); + } + if ( ord( $this->_readstream( $f, 1 ) ) != 0 ) { + $this->Error( 'Interlacing not supported: ' . $file ); + } + $this->_readstream( $f, 4 ); + $dp = '/Predictor 15 /Colors ' . ( $colspace == 'DeviceRGB' ? 3 : 1 ) . ' /BitsPerComponent ' . $bpc . ' /Columns ' . $w; + + // Scan chunks looking for palette, transparency and image data + $pal = ''; + $trns = ''; + $data = ''; + do { + $n = $this->_readint( $f ); + $type = $this->_readstream( $f, 4 ); + if ( $type == 'PLTE' ) { + // Read palette + $pal = $this->_readstream( $f, $n ); + $this->_readstream( $f, 4 ); + } elseif ( $type == 'tRNS' ) { + // Read transparency info + $t = $this->_readstream( $f, $n ); + if ( $ct == 0 ) { + $trns = array( ord( substr( $t, 1, 1 ) ) ); + } elseif ( $ct == 2 ) { + $trns = array( ord( substr( $t, 1, 1 ) ), ord( substr( $t, 3, 1 ) ), ord( substr( $t, 5, 1 ) ) ); + } else { + $pos = strpos( $t, chr( 0 ) ); + if ( $pos !== false ) { + $trns = array( $pos ); + } + } + $this->_readstream( $f, 4 ); + } elseif ( $type == 'IDAT' ) { + // Read image data block + $data .= $this->_readstream( $f, $n ); + $this->_readstream( $f, 4 ); + } elseif ( $type == 'IEND' ) { + break; + } else { + $this->_readstream( $f, $n + 4 ); + } + } while ( $n ); + + if ( $colspace == 'Indexed' && empty( $pal ) ) { + $this->Error( 'Missing palette in ' . $file ); + } + $info = array( + 'w' => $w, + 'h' => $h, + 'cs' => $colspace, + 'bpc' => $bpc, + 'f' => 'FlateDecode', + 'dp' => $dp, + 'pal' => $pal, + 'trns' => $trns + ); + if ( $ct >= 4 ) { + // Extract alpha channel + if ( ! function_exists( 'gzuncompress' ) ) { + $this->Error( 'Zlib not available, can\'t handle alpha channel: ' . $file ); + } + $data = gzuncompress( $data ); + $color = ''; + $alpha = ''; + if ( $ct == 4 ) { + // Gray image + $len = 2 * $w; + for ( $i = 0; $i < $h; $i ++ ) { + $pos = ( 1 + $len ) * $i; + $color .= $data[ $pos ]; + $alpha .= $data[ $pos ]; + $line = substr( $data, $pos + 1, $len ); + $color .= preg_replace( '/(.)./s', '$1', $line ); + $alpha .= preg_replace( '/.(.)/s', '$1', $line ); + } + } else { + // RGB image + $len = 4 * $w; + for ( $i = 0; $i < $h; $i ++ ) { + $pos = ( 1 + $len ) * $i; + $color .= $data[ $pos ]; + $alpha .= $data[ $pos ]; + $line = substr( $data, $pos + 1, $len ); + $color .= preg_replace( '/(.{3})./s', '$1', $line ); + $alpha .= preg_replace( '/.{3}(.)/s', '$1', $line ); + } + } + unset( $data ); + $data = gzcompress( $color ); + $info['smask'] = gzcompress( $alpha ); + $this->WithAlpha = true; + if ( $this->PDFVersion < '1.4' ) { + $this->PDFVersion = '1.4'; + } + } + $info['data'] = $data; + + return $info; + } + + protected function _readstream( $f, $n ) { + // Read n bytes from stream + $res = ''; + while ( $n > 0 && ! feof( $f ) ) { + $s = fread( $f, $n ); + if ( $s === false ) { + $this->Error( 'Error while reading stream' ); + } + $n -= strlen( $s ); + $res .= $s; + } + if ( $n > 0 ) { + $this->Error( 'Unexpected end of stream' ); + } + + return $res; + } + + protected function _readint( $f ) { + // Read a 4-byte integer from stream + $a = unpack( 'Ni', $this->_readstream( $f, 4 ) ); + + return $a['i']; + } + + protected function _parsegif( $file ) { + // Extract info from a GIF file (via PNG conversion) + if ( ! function_exists( 'imagepng' ) ) { + $this->Error( 'GD extension is required for GIF support' ); + } + if ( ! function_exists( 'imagecreatefromgif' ) ) { + $this->Error( 'GD has no GIF read support' ); + } + $im = imagecreatefromgif( $file ); + if ( ! $im ) { + $this->Error( 'Missing or incorrect image file: ' . $file ); + } + imageinterlace( $im, 0 ); + ob_start(); + imagepng( $im ); + $data = ob_get_clean(); + imagedestroy( $im ); + $f = fopen( 'php://temp', 'rb+' ); + if ( ! $f ) { + $this->Error( 'Unable to create memory stream' ); + } + fwrite( $f, $data ); + rewind( $f ); + $info = $this->_parsepngstream( $f, $file ); + fclose( $f ); + + return $info; + } +} + +?> diff --git a/src/Model/Logs.php b/src/Model/Logs.php new file mode 100644 index 0000000..731ee89 --- /dev/null +++ b/src/Model/Logs.php @@ -0,0 +1,184 @@ + 'E_ERROR', + 2 => 'E_WARNING', + 4 => 'E_PARSE', + 8 => 'E_NOTICE', + 16 => 'E_CORE_ERROR', + 32 => 'E_CORE_WARNING', + 64 => 'E_COMPILE_ERROR', + 128 => 'E_COMPILE_WARNING', + 256 => 'E_USER_ERROR', + 512 => 'E_USER_WARNING', + 1024 => 'E_USER_NOTICE', + 2048 => 'E_STRICT', + 4096 => 'E_RECOVERABLE_ERROR', + 8192 => 'E_DEPRECATED', + 16384 => 'E_USER_DEPRECATED', + 32767 => 'E_ALL' + ]; + public $id; + public $level; + public $message; + public $file; + public $line; + public $date; + + /** + * Logs constructor. + * + * @param int|null $id + * @param string|null $level + * @param string|null $message + * @param string|null $file + * @param string|null $line + * @param string|null $date + */ + public function __construct( int $id = null, string $level = null, string $message = null, string $file = null, string $line = null, string $date = null ) { + if ( $id === null ) { + return; + } + $this->id = $id; + $this->level = $level; + $this->message = $message; + $this->file = $file; + $this->line = $line; + $this->date = $date; + } + + + public static function insert( $level, $message, $file, $line, $date ) { + Model::insert( BDTables::LOGS, [ + 'level' => $level, + 'message' => $message, + 'file' => $file, + 'line' => $line, + 'date' => date( "Y-m-d H:i:s", strtotime( $date ) ) + ] ); + } + + /** + * Retourne un tableau des derniers logs (limite en param) + * + * @param int $limit + * + * @return array + */ + public static function getLastLogs( int $limit ) { + $req = BDD::instance()->prepare( 'SELECT * + FROM ' . BDTables::LOGS . ' + ORDER BY date DESC + LIMIT :limit' ); + $req->bindValue( 'limit', $limit, PDO::PARAM_INT ); + $req->execute(); + $return = []; + + foreach ( $req->fetchAll() as $l ) { + $log = new Logs( $l['id'], $l['level'], $l['message'], $l['file'], $l['line'], $l['date'] ); + $return[] = $log; + } + + return $return; + } + + /** + * @return int + */ + public function getId() { + return $this->id; + } + + /** + * @param int $id + */ + public function setId( $id ) { + $this->id = $id; + } + + /** + * @return string + */ + public function getLevel() { + return $this->level; + } + + /** + * @param string $level + */ + public function setLevel( $level ) { + $this->level = $level; + } + + /** + * @return string + */ + public function getMessage() { + return htmlspecialchars( $this->message ); + } + + /** + * @param string $message + */ + public function setMessage( $message ) { + $this->message = $message; + } + + /** + * @return string + */ + public function getFile() { + return htmlspecialchars( $this->file ); + } + + /** + * @param string $file + */ + public function setFile( $file ) { + $this->file = $file; + } + + /** + * @return string + */ + public function getLine() { + return htmlspecialchars( $this->line ); + } + + /** + * @param string $line + */ + public function setLine( $line ) { + $this->line = $line; + } + + /** + * @return string + */ + public function getDate() { + return $this->date; + } + + /** + * @param string $date + */ + public function setDate( $date ) { + $this->date = $date; + } + + /** + * Retourne le type d'erreur en string (label) + * @return string + */ + public function getErrorLabel() { + return htmlspecialchars( self::ERROR_LEVEL[ $this->level ] ); + } +} + +?> \ No newline at end of file diff --git a/src/Model/Model.php b/src/Model/Model.php new file mode 100644 index 0000000..ad4937d --- /dev/null +++ b/src/Model/Model.php @@ -0,0 +1,50 @@ +prepare( 'INSERT INTO ' . $tableName . ' (' . implode( ', ', array_keys( $data ) ) . ') + VALUES (' . ':' . implode( ', :', array_keys( $data ) ) . ')' ); + $req->execute( $data ); + + return BDD::lastInsertId(); + } + + /** + * Met à jour les données d'une ligne d'un table données + * + * @param string $tableName + * @param array $data + * @param string $idColumn + * @param int $idValue + */ + public static function update( string $tableName, array $data, string $idColumn, int $idValue ) { + $reqStr = 'UPDATE ' . $tableName . ' SET '; + $lastKey = endKey( $data ); + foreach ( $data as $key => $value ) { + $reqStr .= $key . ' = :' . $key; + if ( $key != $lastKey ) { + $reqStr .= ', '; + } + } + $reqStr .= ' WHERE ' . $idColumn . ' = :' . $idColumn; + $data[ $idColumn ] = $idValue; + + //echo $reqStr; exit(); + + $req = BDD::instance()->prepare( $reqStr ); + $req->execute( $data ); + } +} + +?> \ No newline at end of file diff --git a/src/View/Error.php b/src/View/Error.php new file mode 100644 index 0000000..e015086 --- /dev/null +++ b/src/View/Error.php @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/View/Site/Index.php b/src/View/Site/Index.php new file mode 100644 index 0000000..0ccb255 --- /dev/null +++ b/src/View/Site/Index.php @@ -0,0 +1,3 @@ +
+ Bonjour 2 +
\ No newline at end of file diff --git a/src/View/Site/SiteError.php b/src/View/Site/SiteError.php new file mode 100644 index 0000000..a44449b --- /dev/null +++ b/src/View/Site/SiteError.php @@ -0,0 +1,6 @@ +
+
+
+
Que voulez-vous faire ?
+
+
diff --git a/src/View/Site/tpl/footer.php b/src/View/Site/tpl/footer.php new file mode 100644 index 0000000..e9063ac --- /dev/null +++ b/src/View/Site/tpl/footer.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/View/Site/tpl/head.php b/src/View/Site/tpl/head.php new file mode 100644 index 0000000..a7275a0 --- /dev/null +++ b/src/View/Site/tpl/head.php @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + <?= $this->head['title'] ?> + + + + + + + + + + + + + + + + + + + head['robotNoIndex'] ) && $this->head['robotNoIndex'] == true ) { ?> + + + + + + diff --git a/src/lib/functions.php b/src/lib/functions.php new file mode 100644 index 0000000..8b4414e --- /dev/null +++ b/src/lib/functions.php @@ -0,0 +1,413 @@ + "e", "&Ecute;" => "E", "à " => "a" ... + $str = preg_replace( '#&([A-za-z])(?:acute|grave|cedil|circ|orn|ring|slash|th|tilde|uml);#', '\1', $str ); + + // Remplacer les ligatures tel que : Œ, Æ ... + // Exemple "Å“" => "oe" + $str = preg_replace( '#&([A-za-z]{2})(?:lig);#', '\1', $str ); + // Supprimer tout le reste + //$str = preg_replace('#&[^;]+;#', '', $str); + $str = str_replace( array( "#", "&", "[", "^", ";", "]" ), '', $str ); + + //on passe tout en minuscule + $str = strtolower( $str ); + + if ( substr( $str, - 1 ) == '_' ) { + $str = substr( $str, 0, - 1 ); + } + + return $str; +} + +/** + * La fonction darkroom() renomme et redimensionne les photos envoyées lors de l'ajout d'un objet. + * + * @param $img String Chemin absolu de l'image d'origine. + * @param $to String Chemin absolu de l'image générée (.jpg). + * @param $width Int Largeur de l'image générée. Si 0, valeur calculée en fonction de $height. + * @param $height Int Hauteur de l'image génétée. Si 0, valeur calculée en fonction de $width. + * Si $height = 0 et $width = 0, dimensions conservées mais conversion en .jpg + * + * @return bool + */ +function darkroom( $img, $to, $width = 0, $height = 0, $quality = 100, $useGD = true ) { + + $dimensions = getimagesize( $img ); + $ratio = $dimensions[0] / $dimensions[1]; + + // Calcul des dimensions si 0 passé en paramètre + if ( $width == 0 && $height == 0 ) { + $width = $dimensions[0]; + $height = $dimensions[1]; + } else if ( $height == 0 ) { + $height = round( $width / $ratio ); + } else if ( $width == 0 ) { + $width = round( $height * $ratio ); + } + + if ( $dimensions[0] > ( $width / $height ) * $dimensions[1] ) { + $dimY = $height; + $dimX = round( $height * $dimensions[0] / $dimensions[1] ); + } + if ( $dimensions[0] < ( $width / $height ) * $dimensions[1] ) { + $dimX = $width; + $dimY = round( $width * $dimensions[1] / $dimensions[0] ); + } + if ( $dimensions[0] == ( $width / $height ) * $dimensions[1] ) { + $dimX = $width; + $dimY = $height; + } + + // Création de l'image avec la librairie GD + if ( $useGD ) { + $pattern = imagecreatetruecolor( $width, $height ); + $type = mime_content_type( $img ); + switch ( substr( $type, 6 ) ) { + case 'jpeg': + $image = imagecreatefromjpeg( $img ); + break; + case 'gif': + $image = imagecreatefromgif( $img ); + break; + case 'png': + $image = imagecreatefrompng( $img ); + break; + } + imagecopyresampled( $pattern, $image, 0, 0, 0, 0, $dimX, $dimY, $dimensions[0], $dimensions[1] ); + imagedestroy( $image ); + imagejpeg( $pattern, $to, $quality ); + + return true; + } + + return true; +} + +/** + * Redéfini la gestion des erreurs + * + * @param $errno + * @param $errstr + * @param $errfile + * @param $errline + * + * @return bool|void + */ +function errorHandler( $errno, $errstr, $errfile, $errline ) { + if ( ! ( error_reporting() & $errno ) ) { + // Ce code d'erreur n'est pas inclus dans error_reporting() + return; + } + + // Insertion des logs + \CAUProject3Contact\Model\Logs::insert( $errno, $errstr, $errfile, $errline, date( 'Y-m-d H:i:s' ) ); + + ob_clean(); + new \CAUProject3Contact\Controller\Site\SiteError( 500 ); + + /* Ne pas exécuter le gestionnaire interne de PHP */ + + return; +} + +/** + * @return array + * Fonction permettant de récupérer des informations sur le navigateur utiliser par l'utilisateur + */ +function getBrowser() { + + $u_agent = $_SERVER['HTTP_USER_AGENT']; + $bname = 'Unknown'; + $platform = 'Unknown'; + $ub = ""; + + //First get the platform? + if ( preg_match( '/android/i', $u_agent ) || preg_match( '/Android/i', $u_agent ) ) { + $platform = 'android'; + } else if ( preg_match( '/linux/i', $u_agent ) ) { + $platform = 'linux'; + } else if ( preg_match( '/macintosh|mac os x/i', $u_agent ) ) { + $platform = 'mac'; + } else if ( preg_match( '/windows|win32/i', $u_agent ) ) { + $platform = 'windows'; + } + + if ( strstr( $u_agent, 'mobile' ) || strstr( $u_agent, 'Mobile' ) ) { + $platform .= ' mobile'; + } + + // Next get the name of the useragent yes seperately and for good reason + if ( preg_match( '/MSIE/i', $u_agent ) && ! preg_match( '/Opera/i', $u_agent ) ) { + $bname = 'Internet Explorer'; + $ub = "MSIE"; + } else if ( preg_match( '/Edge/i', $u_agent ) ) { + $bname = 'Microsoft Edge'; + $ub = "Edge"; + } else if ( preg_match( '/Trident/i', $u_agent ) ) { + $bname = 'Internet Explorer'; + $ub = "rv"; + } else if ( preg_match( '/Firefox/i', $u_agent ) ) { + $bname = 'Mozilla Firefox'; + $ub = "Firefox"; + } else if ( preg_match( '/Chrome/i', $u_agent ) ) { + $bname = 'Google Chrome'; + $ub = "Chrome"; + } else if ( preg_match( '/Safari/i', $u_agent ) ) { + $bname = 'Apple Safari'; + $ub = "Safari"; + } else if ( preg_match( '/Opera/i', $u_agent ) ) { + $bname = 'Opera'; + $ub = "Opera"; + } else if ( preg_match( '/Netscape/i', $u_agent ) ) { + $bname = 'Netscape'; + $ub = "Netscape"; + } + + // finally get the correct version number + // Added "|:" + $known = array( 'Version', $ub, 'other' ); + $pattern = '#(?' . join( '|', $known ) . ')[/|: ]+(?[0-9.|a-zA-Z.]*)#'; + if ( ! preg_match_all( $pattern, $u_agent, $matches ) ) { + // we have no matching number just continue + } + + // see how many we have + $i = count( $matches['browser'] ); + if ( $i != 1 ) { + //we will have two since we are not using 'other' argument yet + //see if version is before or after the name + if ( strripos( $u_agent, "Version" ) < strripos( $u_agent, $ub ) ) { + $version = $matches['version'][0]; + } else { + $version = $matches['version'][1]; + } + } else { + $version = $matches['version'][0]; + } + + // check if we have a number + if ( $version == null || $version == "" ) { + $version = "?"; + } + + return array( + 'userAgent' => $u_agent, + 'platform' => $platform, + 'version' => $version, + 'pattern' => $pattern, + 'name' => $bname + ); +} + +/** + * @param $string + * @param $limit + * + * @return int + */ +function getLimitWord( $string, $limit ) { + $i = $limit; + if ( ! isset( $string ) || empty( $string ) ) { + return 0; + } + while ( $i > 0 && $string[ $i ] != ' ' ) { + $i --; + } + + return $i; +} + +/** + * @param array $destinataires [nom du destinataire => adresse du destinataire] On peut en ajouter autant que l'on veut + * @param string $subject Objet du mail + * @param string $body Corp du mail + * @param string|null $auteurMail L'auteur du mail Par défaut eldotravo@gmail.com + * @param array|null $files [nom du fichier => chemin du fichier] On peut en ajouter autant que l'on veut + * @param array|null $cci [nom du cci => adresse du cci] On peut en ajouter autant que l'on veut + * @param array|null $cc [nom du cc => adresse du cc] On peut en ajouter autant que l'on veut + */ +/* +function email(array $destinataires, string $subject, string $body, string $auteurMail = null, array $files = null, array $cci = null, $cc = null) { + date_default_timezone_set('Etc/UTC'); + + //Create a new PHPMailer instance + $mail = new PHPMailer; + $mail->isSMTP(); + $mail->Host = ""; // Host SMTP du server d'envoi du mail + $mail->SMTPAuth = true; + $mail->Username = ""; // Identifiant de connection + $mail->Password = ""; // Mot de passe de connection + $mail->SMTPSecure = 'ssl'; + $mail->Port = 465; // Port + //Enable SMTP debugging (0 = off (for production use), 1 = client messages, 2 = client and server messages) + $mail->SMTPDebug = 0; + //Ask for HTML-friendly debug output + $mail->Debugoutput = 'html'; + $mail->Sender = ""; // l'email d'envoi + if ($auteurMail == 'Marie-Paule'){ + $mail->setFrom("" , ""); // Mail affiché d'envoi, nom affiché d'envoi + $mail->addReplyTo("", ""); // Mail de reply, nom de reply + } + + // Ajout de tout les utilisateurs + foreach ($destinataires as $nom => $adresseMail){ + if (!empty($adresseMail)) { + $mail->addAddress($adresseMail, $nom); + } + } + + // Ajout de pièces jointes + if (!empty($files)){ + foreach ($files as $name => $file){ + if (file_exists($file)){ + $mail->addAttachment($file, $name); + } + } + } + + // Ajout des CCI + if (!empty($cci)){ + foreach ($cci as $nom => $adresseMail){ + if (!empty($adresseMail)){ + $mail->addBCC($adresseMail, $nom); + } + } + } + + // Ajout des CC + if (!empty($cc)){ + foreach ($cc as $nom => $adresseMail){ + if (!empty($adresseMail)){ + $mail->addCC($adresseMail, $nom); + } + } + } + + $mail->Subject = $subject; + $mail->MsgHTML($body); + //Replace the plain text body with one created manually + $mail->AltBody = ''; + + if (!$mail->send()) { +// echo "Mailer Error: " . $mail->ErrorInfo; + } else { +// echo "Message sent!"; + } +} +*/ + +/** + * @param string $file + * @param int $angle + * @param string $newName + * + * @return bool + */ +function rotateImage( string $file, int $angle, string $newName ) { + // Initialisation variable pou test futur + $image = null; + $type = mime_content_type( $file ); + // Création ressources en fonction de l'image + switch ( substr( $type, 6 ) ) { + case 'jpeg': + $image = imagecreatefromjpeg( $file ); + break; + case 'png': + $image = imagecreatefrompng( $file ); + break; + } + // Si format image non prit en charge + if ( $image == null ) { + return false; + } + // Rotation de l'image + $rotate = imagerotate( $image, $angle, 0 ); + // On recrée l'image au format de base + switch ( substr( $type, 6 ) ) { + case 'jpeg': + imagejpeg( $rotate, $file ); + break; + case 'png': + imagepng( $rotate, $file ); + break; + } + imagedestroy( $image ); + imagedestroy( $rotate ); + rename( $file, $newName ); + + return true; +} + +/** + * @param array $data + * + * @return array + * Clean toutes les strings dans array en récursif, et filtre pour n'avoir qu'un espaces entre chaque mot + */ +function cleanArray( array $data ) { + if ( ! empty( $data ) ) { + foreach ( $data as $key => $donnée ) { + switch ( gettype( $donnée ) ) { + case 'string': + if ( ! empty( $donnée ) ) { + $new_string = ''; + foreach ( explode( ' ', trim( $donnée ) ) as $str ) { + if ( ! empty( $str ) ) { + if ( $new_string != '' ) { + $new_string .= ' '; + } + $new_string .= $str; + } + } + $data[ $key ] = $new_string; + } + break; + case 'array': + if ( ! empty( $donnée ) ) { + $data[ $key ] = cleanArray( $donnée ); + } + break; + } + } + } + + return $data; +} + +/** + * @param $array + * + * @return mixed + */ +function endKey( $array ) { + end( $array ); + + return key( $array ); +} + +?> \ No newline at end of file diff --git a/src/lib/mail/PHPMailerAutoload.php b/src/lib/mail/PHPMailerAutoload.php new file mode 100644 index 0000000..2a82750 --- /dev/null +++ b/src/lib/mail/PHPMailerAutoload.php @@ -0,0 +1,49 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailer SPL autoloader. + * + * @param string $classname The name of the class to load + */ +function PHPMailerAutoload( $classname ) { + //Can't use __DIR__ as it's only in PHP 5.3+ + $filename = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'class.' . strtolower( $classname ) . '.php'; + if ( is_readable( $filename ) ) { + require $filename; + } +} + +if ( version_compare( PHP_VERSION, '5.1.2', '>=' ) ) { + //SPL autoloading was introduced in PHP 5.1.2 + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { + spl_autoload_register( 'PHPMailerAutoload', true, true ); + } else { + spl_autoload_register( 'PHPMailerAutoload' ); + } +} else { + /** + * Fall back to traditional autoload for old PHP versions + * + * @param string $classname The name of the class to load + */ + function __autoload( $classname ) { + PHPMailerAutoload( $classname ); + } +} diff --git a/src/lib/mail/class.phpmailer.php b/src/lib/mail/class.phpmailer.php new file mode 100644 index 0000000..4278591 --- /dev/null +++ b/src/lib/mail/class.phpmailer.php @@ -0,0 +1,3896 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2012 - 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailer - PHP email creation and transport class. + * @package PHPMailer + * @author Marcus Bointon (Synchro/coolbru) + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + */ +class PHPMailer { + /** + * Error severity: message only, continue processing. + */ + const STOP_MESSAGE = 0; + /** + * Error severity: message, likely ok to continue processing. + */ + const STOP_CONTINUE = 1; + /** + * Error severity: message, plus full stop, critical error reached. + */ + const STOP_CRITICAL = 2; + /** + * SMTP RFC standard line ending. + */ + const CRLF = "\r\n"; + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer + */ + const MAX_LINE_LENGTH = 998; + /** + * The PHPMailer Version number. + * @var string + */ + public $Version = '5.2.14'; + /** + * Email priority. + * Options: null (default), 1 = High, 3 = Normal, 5 = low. + * When null, the header is not set at all. + * @var integer + */ + public $Priority = null; + /** + * The character set of the message. + * @var string + */ + public $CharSet = 'utf-8'; + /** + * The MIME Content-type of the message. + * @var string + */ + public $ContentType = 'text/plain'; + /** + * The message encoding. + * Options: "8bit", "7bit", "binary", "base64", and "quoted-printable". + * @var string + */ + public $Encoding = '8bit'; + /** + * Holds the most recent mailer error message. + * @var string + */ + public $ErrorInfo = ''; + /** + * The From email address for the message. + * @var string + */ + public $From = 'root@localhost'; + /** + * The From name of the message. + * @var string + */ + public $FromName = 'Root User'; + /** + * The Sender email (Return-Path) of the message. + * If not empty, will be sent via -f to sendmail or as 'MAIL FROM' in smtp mode. + * @var string + */ + public $Sender = ''; + /** + * The Return-Path of the message. + * If empty, it will be set to either From or Sender. + * @var string + * @deprecated Email senders should never set a return-path header; + * it's the receiver's job (RFC5321 section 4.4), so this no longer does anything. + * @link https://tools.ietf.org/html/rfc5321#section-4.4 RFC5321 reference + */ + public $ReturnPath = ''; + /** + * The Subject of the message. + * @var string + */ + public $Subject = ''; + /** + * An HTML or plain text message body. + * If HTML then call isHTML(true). + * @var string + */ + public $Body = ''; + /** + * The plain-text message body. + * This body can be read by mail clients that do not have HTML email + * capability such as mutt & Eudora. + * Clients that can read HTML will view the normal Body. + * @var string + */ + public $AltBody = ''; + /** + * An iCal message part body. + * Only supported in simple alt or alt_inline message types + * To generate iCal events, use the bundled extras/EasyPeasyICS.php class or iCalcreator + * @link http://sprain.ch/blog/downloads/php-class-easypeasyics-create-ical-files-with-php/ + * @link http://kigkonsult.se/iCalcreator/ + * @var string + */ + public $Ical = ''; + /** + * Word-wrap the message body to this number of chars. + * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * @var integer + */ + public $WordWrap = 0; + /** + * Which method to use to send mail. + * Options: "mail", "sendmail", or "smtp". + * @var string + */ + public $Mailer = 'mail'; + /** + * The path to the sendmail program. + * @var string + */ + public $Sendmail = '/usr/sbin/sendmail'; + /** + * Whether mail() uses a fully sendmail-compatible MTA. + * One which supports sendmail's "-oi -f" options. + * @var boolean + */ + public $UseSendmailOptions = true; + /** + * Path to PHPMailer plugins. + * Useful if the SMTP class is not in the PHP include path. + * @var string + * @deprecated Should not be needed now there is an autoloader. + */ + public $PluginDir = ''; + /** + * The email address that a reading confirmation should be sent to, also known as read receipt. + * @var string + */ + public $ConfirmReadingTo = ''; + /** + * The hostname to use in the Message-ID header and as default HELO string. + * If empty, PHPMailer attempts to find one with, in order, + * $_SERVER['SERVER_NAME'], gethostname(), php_uname('n'), or the value + * 'localhost.localdomain'. + * @var string + */ + public $Hostname = ''; + /** + * An ID to be used in the Message-ID header. + * If empty, a unique id will be generated. + * @var string + */ + public $MessageID = ''; + /** + * The message Date to be used in the Date header. + * If empty, the current date will be added. + * @var string + */ + public $MessageDate = ''; + /** + * SMTP hosts. + * Either a single hostname or multiple semicolon-delimited hostnames. + * You can also specify a different port + * for each host by using this format: [hostname:port] + * (e.g. "smtp1.example.com:25;smtp2.example.com"). + * You can also specify encryption type, for example: + * (e.g. "tls://smtp1.example.com:587;ssl://smtp2.example.com:465"). + * Hosts will be tried in order. + * @var string + */ + public $Host = 'localhost'; + /** + * The default SMTP server port. + * @var integer + */ + public $Port = 25; + /** + * The SMTP HELO of the message. + * Default is $Hostname. If $Hostname is empty, PHPMailer attempts to find + * one with the same method described above for $Hostname. + * @var string + * @see PHPMailer::$Hostname + */ + public $Helo = ''; + /** + * What kind of encryption to use on the SMTP connection. + * Options: '', 'ssl' or 'tls' + * @var string + */ + public $SMTPSecure = ''; + /** + * Whether to enable TLS encryption automatically if a server supports it, + * even if `SMTPSecure` is not set to 'tls'. + * Be aware that in PHP >= 5.6 this requires that the server's certificates are valid. + * @var boolean + */ + public $SMTPAutoTLS = true; + /** + * Whether to use SMTP authentication. + * Uses the Username and Password properties. + * @var boolean + * @see PHPMailer::$Username + * @see PHPMailer::$Password + */ + public $SMTPAuth = false; + /** + * Options array passed to stream_context_create when connecting via SMTP. + * @var array + */ + public $SMTPOptions = array(); + /** + * SMTP username. + * @var string + */ + public $Username = ''; + /** + * SMTP password. + * @var string + */ + public $Password = ''; + /** + * SMTP auth type. + * Options are LOGIN (default), PLAIN, NTLM, CRAM-MD5 + * @var string + */ + public $AuthType = ''; + /** + * SMTP realm. + * Used for NTLM auth + * @var string + */ + public $Realm = ''; + /** + * SMTP workstation. + * Used for NTLM auth + * @var string + */ + public $Workstation = ''; + /** + * The SMTP server timeout in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer + */ + public $Timeout = 300; + /** + * SMTP class debug output mode. + * Debug output level. + * Options: + * * `0` No output + * * `1` Commands + * * `2` Data and commands + * * `3` As 2 plus connection status + * * `4` Low-level data output + * @var integer + * @see SMTP::$do_debug + */ + public $SMTPDebug = 0; + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $mail->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable + * @see SMTP::$Debugoutput + */ + public $Debugoutput = 'echo'; + /** + * Whether to keep SMTP connection open after each message. + * If this is set to true then to close the connection + * requires an explicit call to smtpClose(). + * @var boolean + */ + public $SMTPKeepAlive = false; + /** + * Whether to split multiple to addresses into multiple messages + * or send them all in one message. + * Only supported in `mail` and `sendmail` transports, not in SMTP. + * @var boolean + */ + public $SingleTo = false; + /** + * Storage for addresses when SingleTo is enabled. + * @var array + */ + public $SingleToArray = array(); + /** + * Whether to generate VERP addresses on send. + * Only applicable when sending via SMTP. + * @link https://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Postfix VERP info + * @var boolean + */ + public $do_verp = false; + /** + * Whether to allow sending messages with an empty body. + * @var boolean + */ + public $AllowEmpty = false; + /** + * The default line ending. + * @note The default remains "\n". We force CRLF where we know + * it must be used via self::CRLF. + * @var string + */ + public $LE = "\n"; + /** + * DKIM selector. + * @var string + */ + public $DKIM_selector = ''; + /** + * DKIM Identity. + * Usually the email address used as the source of the email + * @var string + */ + public $DKIM_identity = ''; + /** + * DKIM passphrase. + * Used if your key is encrypted. + * @var string + */ + public $DKIM_passphrase = ''; + /** + * DKIM signing domain name. + * @example 'example.com' + * @var string + */ + public $DKIM_domain = ''; + /** + * DKIM private key file path. + * @var string + */ + public $DKIM_private = ''; + /** + * Callback Action function name. + * + * The function that handles the result of the send email action. + * It is called out by send() for each email sent. + * + * Value can be any php callable: http://www.php.net/is_callable + * + * Parameters: + * boolean $result result of the send action + * string $to email address of the recipient + * string $cc cc email addresses + * string $bcc bcc email addresses + * string $subject the subject + * string $body the email body + * string $from email address of sender + * @var string + */ + public $action_function = ''; + /** + * What to put in the X-Mailer header. + * Options: An empty string for PHPMailer default, whitespace for none, or a string to use + * @var string + */ + public $XMailer = ''; + /** + * The complete compiled MIME message body. + * @access protected + * @var string + */ + protected $MIMEBody = ''; + /** + * The complete compiled MIME message headers. + * @var string + * @access protected + */ + protected $MIMEHeader = ''; + /** + * Extra headers that createHeader() doesn't fold in. + * @var string + * @access protected + */ + protected $mailHeader = ''; + /** + * An instance of the SMTP sender class. + * @var SMTP + * @access protected + */ + protected $smtp = null; + /** + * The array of 'to' names and addresses. + * @var array + * @access protected + */ + protected $to = array(); + /** + * The array of 'cc' names and addresses. + * @var array + * @access protected + */ + protected $cc = array(); + /** + * The array of 'bcc' names and addresses. + * @var array + * @access protected + */ + protected $bcc = array(); + /** + * The array of reply-to names and addresses. + * @var array + * @access protected + */ + protected $ReplyTo = array(); + /** + * An array of all kinds of addresses. + * Includes all of $to, $cc, $bcc + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + */ + protected $all_recipients = array(); + /** + * An array of names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $all_recipients + * and one of $to, $cc, or $bcc. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$to @see PHPMailer::$cc @see PHPMailer::$bcc + * @see PHPMailer::$all_recipients + */ + protected $RecipientsQueue = array(); + /** + * An array of reply-to names and addresses queued for validation. + * In send(), valid and non duplicate entries are moved to $ReplyTo. + * This array is used only for addresses with IDN. + * @var array + * @access protected + * @see PHPMailer::$ReplyTo + */ + protected $ReplyToQueue = array(); + /** + * The array of attachments. + * @var array + * @access protected + */ + protected $attachment = array(); + /** + * The array of custom headers. + * @var array + * @access protected + */ + protected $CustomHeader = array(); + /** + * The most recent Message-ID (including angular brackets). + * @var string + * @access protected + */ + protected $lastMessageID = ''; + /** + * The message's MIME type. + * @var string + * @access protected + */ + protected $message_type = ''; + /** + * The array of MIME boundary strings. + * @var array + * @access protected + */ + protected $boundary = array(); + /** + * The array of available languages. + * @var array + * @access protected + */ + protected $language = array(); + /** + * The number of errors encountered. + * @var integer + * @access protected + */ + protected $error_count = 0; + /** + * The S/MIME certificate file path. + * @var string + * @access protected + */ + protected $sign_cert_file = ''; + /** + * The S/MIME key file path. + * @var string + * @access protected + */ + protected $sign_key_file = ''; + /** + * The optional S/MIME extra certificates ("CA Chain") file path. + * @var string + * @access protected + */ + protected $sign_extracerts_file = ''; + /** + * The S/MIME password for the key. + * Used only if the key is encrypted. + * @var string + * @access protected + */ + protected $sign_key_pass = ''; + /** + * Whether to throw exceptions for errors. + * @var boolean + * @access protected + */ + protected $exceptions = false; + /** + * Unique ID used for message ID and boundaries. + * @var string + * @access protected + */ + protected $uniqueid = ''; + + /** + * Constructor. + * + * @param boolean $exceptions Should we throw external exceptions? + */ + public function __construct( $exceptions = false ) { + $this->exceptions = (boolean) $exceptions; + } + + /** + * Destructor. + */ + public function __destruct() { + //Close any open SMTP connection nicely + $this->smtpClose(); + } + + /** + * Close the active SMTP session if one exists. + * @return void + */ + public function smtpClose() { + if ( is_a( $this->smtp, 'SMTP' ) ) { + if ( $this->smtp->connected() ) { + $this->smtp->quit(); + $this->smtp->close(); + } + } + } + + /** + * Send messages using SMTP. + * @return void + */ + public function isSMTP() { + $this->Mailer = 'smtp'; + } + + /** + * Send messages using PHP's mail() function. + * @return void + */ + public function isMail() { + $this->Mailer = 'mail'; + } + + /** + * Send messages using $Sendmail. + * @return void + */ + public function isSendmail() { + $ini_sendmail_path = ini_get( 'sendmail_path' ); + + if ( ! stristr( $ini_sendmail_path, 'sendmail' ) ) { + $this->Sendmail = '/usr/sbin/sendmail'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'sendmail'; + } + + /** + * Send messages using qmail. + * @return void + */ + public function isQmail() { + $ini_sendmail_path = ini_get( 'sendmail_path' ); + + if ( ! stristr( $ini_sendmail_path, 'qmail' ) ) { + $this->Sendmail = '/var/qmail/bin/qmail-inject'; + } else { + $this->Sendmail = $ini_sendmail_path; + } + $this->Mailer = 'qmail'; + } + + /** + * Add a "To" address. + * + * @param string $address The email address to send to + * @param string $name + * + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addAddress( $address, $name = '' ) { + return $this->addOrEnqueueAnAddress( 'to', $address, $name ); + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. Because PHPMailer + * can't validate addresses with an IDN without knowing the PHPMailer::$CharSet (that can still + * be modified after calling this function), addition of such addresses is delayed until send(). + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addOrEnqueueAnAddress( $kind, $address, $name ) { + $address = trim( $address ); + $name = trim( preg_replace( '/[\r\n]+/', '', $name ) ); //Strip breaks and trim + if ( ( $pos = strrpos( $address, '@' ) ) === false ) { + // At-sign is misssing. + $error_message = $this->lang( 'invalid_address' ) . " (addAnAddress $kind): $address"; + $this->setError( $error_message ); + $this->edebug( $error_message ); + if ( $this->exceptions ) { + throw new phpmailerException( $error_message ); + } + + return false; + } + $params = array( $kind, $address, $name ); + // Enqueue addresses with IDN until we know the PHPMailer::$CharSet. + if ( $this->has8bitChars( substr( $address, ++ $pos ) ) and $this->idnSupported() ) { + if ( $kind != 'Reply-To' ) { + if ( ! array_key_exists( $address, $this->RecipientsQueue ) ) { + $this->RecipientsQueue[ $address ] = $params; + + return true; + } + } else { + if ( ! array_key_exists( $address, $this->ReplyToQueue ) ) { + $this->ReplyToQueue[ $address ] = $params; + + return true; + } + } + + return false; + } + + // Immediately add standard addresses without IDN. + return call_user_func_array( array( $this, 'addAnAddress' ), $params ); + } + + /** + * Get an error message in the current language. + * @access protected + * + * @param string $key + * + * @return string + */ + protected function lang( $key ) { + if ( count( $this->language ) < 1 ) { + $this->setLanguage( 'en' ); // set the default language + } + + if ( array_key_exists( $key, $this->language ) ) { + if ( $key == 'smtp_connect_failed' ) { + //Include a link to troubleshooting docs on SMTP connection failure + //this is by far the biggest cause of support questions + //but it's usually not PHPMailer's fault. + return $this->language[ $key ] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + } + + return $this->language[ $key ]; + } else { + //Return the key as a fallback + return $key; + } + } + + /** + * Set the language for error messages. + * Returns false if it cannot load the language file. + * The default language is English. + * + * @param string $langcode ISO 639-1 2-character language code (e.g. French is "fr") + * @param string $lang_path Path to the language file directory, with trailing separator (slash) + * + * @return boolean + * @access public + */ + public function setLanguage( $langcode = 'en', $lang_path = '' ) { + // Define full set of translatable strings in English + $PHPMAILER_LANG = array( + 'authenticate' => 'SMTP Error: Could not authenticate.', + 'connect_host' => 'SMTP Error: Could not connect to SMTP host.', + 'data_not_accepted' => 'SMTP Error: data not accepted.', + 'empty_message' => 'Message body empty', + 'encoding' => 'Unknown encoding: ', + 'execute' => 'Could not execute: ', + 'file_access' => 'Could not access file: ', + 'file_open' => 'File Error: Could not open file: ', + 'from_failed' => 'The following From address failed: ', + 'instantiate' => 'Could not instantiate mail function.', + 'invalid_address' => 'Invalid address: ', + 'mailer_not_supported' => ' mailer is not supported.', + 'provide_address' => 'You must provide at least one recipient email address.', + 'recipients_failed' => 'SMTP Error: The following recipients failed: ', + 'signing' => 'Signing Error: ', + 'smtp_connect_failed' => 'SMTP connect() failed.', + 'smtp_error' => 'SMTP server error: ', + 'variable_set' => 'Cannot set or reset variable: ', + 'extension_missing' => 'Extension missing: ' + ); + if ( empty( $lang_path ) ) { + // Calculate an absolute path so it can work if CWD is not here + $lang_path = dirname( __FILE__ ) . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR; + } + $foundlang = true; + $lang_file = $lang_path . 'phpmailer.lang-' . $langcode . '.php'; + // There is no English translation file + if ( $langcode != 'en' ) { + // Make sure language file path is readable + if ( ! is_readable( $lang_file ) ) { + $foundlang = false; + } else { + // Overwrite language-specific strings. + // This way we'll never have missing translation keys. + $foundlang = include $lang_file; + } + } + $this->language = $PHPMAILER_LANG; + + return (boolean) $foundlang; // Returns false if language not found + } + + /** + * Add an error message to the error container. + * @access protected + * + * @param string $msg + * + * @return void + */ + protected function setError( $msg ) { + $this->error_count ++; + if ( $this->Mailer == 'smtp' and ! is_null( $this->smtp ) ) { + $lasterror = $this->smtp->getError(); + if ( ! empty( $lasterror['error'] ) ) { + $msg .= $this->lang( 'smtp_error' ) . $lasterror['error']; + if ( ! empty( $lasterror['detail'] ) ) { + $msg .= ' Detail: ' . $lasterror['detail']; + } + if ( ! empty( $lasterror['smtp_code'] ) ) { + $msg .= ' SMTP code: ' . $lasterror['smtp_code']; + } + if ( ! empty( $lasterror['smtp_code_ex'] ) ) { + $msg .= ' Additional SMTP info: ' . $lasterror['smtp_code_ex']; + } + } + } + $this->ErrorInfo = $msg; + } + + /** + * Output debugging info via user-defined method. + * Only generates output if SMTP debug output is enabled (@see SMTP::$do_debug). + * @see PHPMailer::$Debugoutput + * @see PHPMailer::$SMTPDebug + * + * @param string $str + */ + protected function edebug( $str ) { + if ( $this->SMTPDebug <= 0 ) { + return; + } + //Avoid clash with built-in function names + if ( ! in_array( $this->Debugoutput, array( + 'error_log', + 'html', + 'echo' + ) ) and is_callable( $this->Debugoutput ) ) { + call_user_func( $this->Debugoutput, $str, $this->SMTPDebug ); + + return; + } + switch ( $this->Debugoutput ) { + case 'error_log': + //Don't output, just log + error_log( $str ); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace( '/[\r\n]+/', '', $str ), + ENT_QUOTES, + 'UTF-8' + ) + . "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace( '/\r\n?/ms', "\n", $str ); + echo gmdate( 'Y-m-d H:i:s' ) . "\t" . str_replace( + "\n", + "\n \t ", + trim( $str ) + ) . "\n"; + } + } + + /** + * Does a string contain any 8-bit chars (in any charset)? + * + * @param string $text + * + * @return boolean + */ + public function has8bitChars( $text ) { + return (boolean) preg_match( '/[\x80-\xFF]/', $text ); + } + + /** + * Tells whether IDNs (Internationalized Domain Names) are supported or not. This requires the + * "intl" and "mbstring" PHP extensions. + * @return bool "true" if required functions for IDN support are present + */ + public function idnSupported() { + return function_exists( 'idn_to_ascii' ) and function_exists( 'mb_convert_encoding' ); + } + + /** + * Add a "CC" address. + * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. + * + * @param string $address The email address to send to + * @param string $name + * + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addCC( $address, $name = '' ) { + return $this->addOrEnqueueAnAddress( 'cc', $address, $name ); + } + + /** + * Add a "BCC" address. + * @note: This function works with the SMTP mailer on win32, not with the "mail" mailer. + * + * @param string $address The email address to send to + * @param string $name + * + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addBCC( $address, $name = '' ) { + return $this->addOrEnqueueAnAddress( 'bcc', $address, $name ); + } + + /** + * Add a "Reply-To" address. + * + * @param string $address The email address to reply to + * @param string $name + * + * @return boolean true on success, false if address already used or invalid in some way + */ + public function addReplyTo( $address, $name = '' ) { + return $this->addOrEnqueueAnAddress( 'Reply-To', $address, $name ); + } + + /** + * Parse and validate a string containing one or more RFC822-style comma-separated email addresses + * of the form "display name
" into an array of name/address pairs. + * Uses the imap_rfc822_parse_adrlist function if the IMAP extension is available. + * Note that quotes in the name part are removed. + * + * @param string $addrstr The address list string + * @param bool $useimap Whether to use the IMAP extension to parse the list + * + * @return array + * @link http://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation + */ + public function parseAddresses( $addrstr, $useimap = true ) { + $addresses = array(); + if ( $useimap and function_exists( 'imap_rfc822_parse_adrlist' ) ) { + //Use this built-in parser if it's available + $list = imap_rfc822_parse_adrlist( $addrstr, '' ); + foreach ( $list as $address ) { + if ( $address->host != '.SYNTAX-ERROR.' ) { + if ( $this->validateAddress( $address->mailbox . '@' . $address->host ) ) { + $addresses[] = array( + 'name' => ( property_exists( $address, 'personal' ) ? $address->personal : '' ), + 'address' => $address->mailbox . '@' . $address->host + ); + } + } + } + } else { + //Use this simpler parser + $list = explode( ',', $addrstr ); + foreach ( $list as $address ) { + $address = trim( $address ); + //Is there a separate name part? + if ( strpos( $address, '<' ) === false ) { + //No separate name, just use the whole thing + if ( $this->validateAddress( $address ) ) { + $addresses[] = array( + 'name' => '', + 'address' => $address + ); + } + } else { + list( $name, $email ) = explode( '<', $address ); + $email = trim( str_replace( '>', '', $email ) ); + if ( $this->validateAddress( $email ) ) { + $addresses[] = array( + 'name' => trim( str_replace( array( '"', "'" ), '', $name ) ), + 'address' => $email + ); + } + } + } + } + + return $addresses; + } + + /** + * Set the From and FromName properties. + * + * @param string $address + * @param string $name + * @param boolean $auto Whether to also set the Sender address, defaults to true + * + * @throws phpmailerException + * @return boolean + */ + public function setFrom( $address, $name = '', $auto = true ) { + $address = trim( $address ); + $name = trim( preg_replace( '/[\r\n]+/', '', $name ) ); //Strip breaks and trim + // Don't validate now addresses with IDN. Will be done in send(). + if ( ( $pos = strrpos( $address, '@' ) ) === false or + ( ! $this->has8bitChars( substr( $address, ++ $pos ) ) or ! $this->idnSupported() ) and + ! $this->validateAddress( $address ) ) { + $error_message = $this->lang( 'invalid_address' ) . " (setFrom) $address"; + $this->setError( $error_message ); + $this->edebug( $error_message ); + if ( $this->exceptions ) { + throw new phpmailerException( $error_message ); + } + + return false; + } + $this->From = $address; + $this->FromName = $name; + if ( $auto ) { + if ( empty( $this->Sender ) ) { + $this->Sender = $address; + } + } + + return true; + } + + /** + * Return the Message-ID header of the last email. + * Technically this is the value from the last time the headers were created, + * but it's also the message ID of the last sent message except in + * pathological cases. + * @return string + */ + public function getLastMessageID() { + return $this->lastMessageID; + } + + /** + * Create a message and send it. + * Uses the sending method specified by $Mailer. + * @throws phpmailerException + * @return boolean false on error - See the ErrorInfo property for details of the error. + */ + public function send() { + try { + if ( ! $this->preSend() ) { + return false; + } + + return $this->postSend(); + } catch ( phpmailerException $exc ) { + $this->mailHeader = ''; + $this->setError( $exc->getMessage() ); + if ( $this->exceptions ) { + throw $exc; + } + + return false; + } + } + + /** + * Prepare a message for sending. + * @throws phpmailerException + * @return boolean + */ + public function preSend() { + try { + $this->error_count = 0; // Reset errors + $this->mailHeader = ''; + + // Dequeue recipient and Reply-To addresses with IDN + foreach ( array_merge( $this->RecipientsQueue, $this->ReplyToQueue ) as $params ) { + $params[1] = $this->punyencodeAddress( $params[1] ); + call_user_func_array( array( $this, 'addAnAddress' ), $params ); + } + if ( ( count( $this->to ) + count( $this->cc ) + count( $this->bcc ) ) < 1 ) { + throw new phpmailerException( $this->lang( 'provide_address' ), self::STOP_CRITICAL ); + } + + // Validate From, Sender, and ConfirmReadingTo addresses + foreach ( array( 'From', 'Sender', 'ConfirmReadingTo' ) as $address_kind ) { + $this->$address_kind = trim( $this->$address_kind ); + if ( empty( $this->$address_kind ) ) { + continue; + } + $this->$address_kind = $this->punyencodeAddress( $this->$address_kind ); + if ( ! $this->validateAddress( $this->$address_kind ) ) { + $error_message = $this->lang( 'invalid_address' ) . ' (punyEncode) ' . $this->$address_kind; + $this->setError( $error_message ); + $this->edebug( $error_message ); + if ( $this->exceptions ) { + throw new phpmailerException( $error_message ); + } + + return false; + } + } + + // Set whether the message is multipart/alternative + if ( $this->alternativeExists() ) { + $this->ContentType = 'multipart/alternative'; + } + + $this->setMessageType(); + // Refuse to send an empty message unless we are specifically allowing it + if ( ! $this->AllowEmpty and empty( $this->Body ) ) { + throw new phpmailerException( $this->lang( 'empty_message' ), self::STOP_CRITICAL ); + } + + // Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) + $this->MIMEHeader = ''; + $this->MIMEBody = $this->createBody(); + // createBody may have added some headers, so retain them + $tempheaders = $this->MIMEHeader; + $this->MIMEHeader = $this->createHeader(); + $this->MIMEHeader .= $tempheaders; + + // To capture the complete message when using mail(), create + // an extra header list which createHeader() doesn't fold in + if ( $this->Mailer == 'mail' ) { + if ( count( $this->to ) > 0 ) { + $this->mailHeader .= $this->addrAppend( 'To', $this->to ); + } else { + $this->mailHeader .= $this->headerLine( 'To', 'undisclosed-recipients:;' ); + } + $this->mailHeader .= $this->headerLine( + 'Subject', + $this->encodeHeader( $this->secureHeader( trim( $this->Subject ) ) ) + ); + } + + // Sign with DKIM if enabled + if ( ! empty( $this->DKIM_domain ) + && ! empty( $this->DKIM_private ) + && ! empty( $this->DKIM_selector ) + && file_exists( $this->DKIM_private ) ) { + $header_dkim = $this->DKIM_Add( + $this->MIMEHeader . $this->mailHeader, + $this->encodeHeader( $this->secureHeader( $this->Subject ) ), + $this->MIMEBody + ); + $this->MIMEHeader = rtrim( $this->MIMEHeader, "\r\n " ) . self::CRLF . + str_replace( "\r\n", "\n", $header_dkim ) . self::CRLF; + } + + return true; + } catch ( phpmailerException $exc ) { + $this->setError( $exc->getMessage() ); + if ( $this->exceptions ) { + throw $exc; + } + + return false; + } + } + + /** + * Converts IDN in given email address to its ASCII form, also known as punycode, if possible. + * Important: Address must be passed in same encoding as currently set in PHPMailer::$CharSet. + * This function silently returns unmodified address if: + * - No conversion is necessary (i.e. domain name is not an IDN, or is already in ASCII form) + * - Conversion to punycode is impossible (e.g. required PHP functions are not available) + * or fails for any reason (e.g. domain has characters not allowed in an IDN) + * @see PHPMailer::$CharSet + * + * @param string $address The email address to convert + * + * @return string The encoded address in ASCII form + */ + public function punyencodeAddress( $address ) { + // Verify we have required functions, CharSet, and at-sign. + if ( $this->idnSupported() and + ! empty( $this->CharSet ) and + ( $pos = strrpos( $address, '@' ) ) !== false ) { + $domain = substr( $address, ++ $pos ); + // Verify CharSet string is a valid one, and domain properly encoded in this CharSet. + if ( $this->has8bitChars( $domain ) and @mb_check_encoding( $domain, $this->CharSet ) ) { + $domain = mb_convert_encoding( $domain, 'UTF-8', $this->CharSet ); + if ( ( $punycode = defined( 'INTL_IDNA_VARIANT_UTS46' ) ? + idn_to_ascii( $domain, 0, INTL_IDNA_VARIANT_UTS46 ) : + idn_to_ascii( $domain ) ) !== false ) { + return substr( $address, 0, $pos ) . $punycode; + } + } + } + + return $address; + } + + /** + * Check if this message has an alternative body set. + * @return boolean + */ + public function alternativeExists() { + return ! empty( $this->AltBody ); + } + + /** + * Set the message type. + * PHPMailer only supports some preset message types, + * not arbitrary MIME structures. + * @access protected + * @return void + */ + protected function setMessageType() { + $type = array(); + if ( $this->alternativeExists() ) { + $type[] = 'alt'; + } + if ( $this->inlineImageExists() ) { + $type[] = 'inline'; + } + if ( $this->attachmentExists() ) { + $type[] = 'attach'; + } + $this->message_type = implode( '_', $type ); + if ( $this->message_type == '' ) { + $this->message_type = 'plain'; + } + } + + /** + * Check if an inline attachment is present. + * @access public + * @return boolean + */ + public function inlineImageExists() { + foreach ( $this->attachment as $attachment ) { + if ( $attachment[6] == 'inline' ) { + return true; + } + } + + return false; + } + + /** + * Check if an attachment (non-inline) is present. + * @return boolean + */ + public function attachmentExists() { + foreach ( $this->attachment as $attachment ) { + if ( $attachment[6] == 'attachment' ) { + return true; + } + } + + return false; + } + + /** + * Assemble the message body. + * Returns an empty string on failure. + * @access public + * @throws phpmailerException + * @return string The assembled message body + */ + public function createBody() { + $body = ''; + //Create unique IDs and preset boundaries + $this->uniqueid = md5( uniqid( time() ) ); + $this->boundary[1] = 'b1_' . $this->uniqueid; + $this->boundary[2] = 'b2_' . $this->uniqueid; + $this->boundary[3] = 'b3_' . $this->uniqueid; + + if ( $this->sign_key_file ) { + $body .= $this->getMailMIME() . $this->LE; + } + + $this->setWordWrap(); + + $bodyEncoding = $this->Encoding; + $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ( $bodyEncoding == '8bit' and ! $this->has8bitChars( $this->Body ) ) { + $bodyEncoding = '7bit'; + $bodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding + if ( 'base64' != $this->Encoding and self::hasLineLongerThanMax( $this->Body ) ) { + $this->Encoding = 'quoted-printable'; + $bodyEncoding = 'quoted-printable'; + } + + $altBodyEncoding = $this->Encoding; + $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? + if ( $altBodyEncoding == '8bit' and ! $this->has8bitChars( $this->AltBody ) ) { + $altBodyEncoding = '7bit'; + $altBodyCharSet = 'us-ascii'; + } + //If lines are too long, and we're not already using an encoding that will shorten them, + //change to quoted-printable transfer encoding + if ( 'base64' != $altBodyEncoding and self::hasLineLongerThanMax( $this->AltBody ) ) { + $altBodyEncoding = 'quoted-printable'; + } + //Use this as a preamble in all multipart message types + $mimepre = "This is a multi-part message in MIME format." . $this->LE . $this->LE; + switch ( $this->message_type ) { + case 'inline': + $body .= $mimepre; + $body .= $this->getBoundary( $this->boundary[1], $bodyCharSet, '', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll( 'inline', $this->boundary[1] ); + break; + case 'attach': + $body .= $mimepre; + $body .= $this->getBoundary( $this->boundary[1], $bodyCharSet, '', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll( 'attachment', $this->boundary[1] ); + break; + case 'inline_attach': + $body .= $mimepre; + $body .= $this->textLine( '--' . $this->boundary[1] ); + $body .= $this->headerLine( 'Content-Type', 'multipart/related;' ); + $body .= $this->textLine( "\tboundary=\"" . $this->boundary[2] . '"' ); + $body .= $this->LE; + $body .= $this->getBoundary( $this->boundary[2], $bodyCharSet, '', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll( 'inline', $this->boundary[2] ); + $body .= $this->LE; + $body .= $this->attachAll( 'attachment', $this->boundary[1] ); + break; + case 'alt': + $body .= $mimepre; + $body .= $this->getBoundary( $this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding ); + $body .= $this->encodeString( $this->AltBody, $altBodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->getBoundary( $this->boundary[1], $bodyCharSet, 'text/html', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + if ( ! empty( $this->Ical ) ) { + $body .= $this->getBoundary( $this->boundary[1], '', 'text/calendar; method=REQUEST', '' ); + $body .= $this->encodeString( $this->Ical, $this->Encoding ); + $body .= $this->LE . $this->LE; + } + $body .= $this->endBoundary( $this->boundary[1] ); + break; + case 'alt_inline': + $body .= $mimepre; + $body .= $this->getBoundary( $this->boundary[1], $altBodyCharSet, 'text/plain', $altBodyEncoding ); + $body .= $this->encodeString( $this->AltBody, $altBodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->textLine( '--' . $this->boundary[1] ); + $body .= $this->headerLine( 'Content-Type', 'multipart/related;' ); + $body .= $this->textLine( "\tboundary=\"" . $this->boundary[2] . '"' ); + $body .= $this->LE; + $body .= $this->getBoundary( $this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll( 'inline', $this->boundary[2] ); + $body .= $this->LE; + $body .= $this->endBoundary( $this->boundary[1] ); + break; + case 'alt_attach': + $body .= $mimepre; + $body .= $this->textLine( '--' . $this->boundary[1] ); + $body .= $this->headerLine( 'Content-Type', 'multipart/alternative;' ); + $body .= $this->textLine( "\tboundary=\"" . $this->boundary[2] . '"' ); + $body .= $this->LE; + $body .= $this->getBoundary( $this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding ); + $body .= $this->encodeString( $this->AltBody, $altBodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->getBoundary( $this->boundary[2], $bodyCharSet, 'text/html', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->endBoundary( $this->boundary[2] ); + $body .= $this->LE; + $body .= $this->attachAll( 'attachment', $this->boundary[1] ); + break; + case 'alt_inline_attach': + $body .= $mimepre; + $body .= $this->textLine( '--' . $this->boundary[1] ); + $body .= $this->headerLine( 'Content-Type', 'multipart/alternative;' ); + $body .= $this->textLine( "\tboundary=\"" . $this->boundary[2] . '"' ); + $body .= $this->LE; + $body .= $this->getBoundary( $this->boundary[2], $altBodyCharSet, 'text/plain', $altBodyEncoding ); + $body .= $this->encodeString( $this->AltBody, $altBodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->textLine( '--' . $this->boundary[2] ); + $body .= $this->headerLine( 'Content-Type', 'multipart/related;' ); + $body .= $this->textLine( "\tboundary=\"" . $this->boundary[3] . '"' ); + $body .= $this->LE; + $body .= $this->getBoundary( $this->boundary[3], $bodyCharSet, 'text/html', $bodyEncoding ); + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + $body .= $this->LE . $this->LE; + $body .= $this->attachAll( 'inline', $this->boundary[3] ); + $body .= $this->LE; + $body .= $this->endBoundary( $this->boundary[2] ); + $body .= $this->LE; + $body .= $this->attachAll( 'attachment', $this->boundary[1] ); + break; + default: + // catch case 'plain' and case '' + $body .= $this->encodeString( $this->Body, $bodyEncoding ); + break; + } + + if ( $this->isError() ) { + $body = ''; + } elseif ( $this->sign_key_file ) { + try { + if ( ! defined( 'PKCS7_TEXT' ) ) { + throw new phpmailerException( $this->lang( 'extension_missing' ) . 'openssl' ); + } + $file = tempnam( sys_get_temp_dir(), 'mail' ); + if ( false === file_put_contents( $file, $body ) ) { + throw new phpmailerException( $this->lang( 'signing' ) . ' Could not write temp file' ); + } + $signed = tempnam( sys_get_temp_dir(), 'signed' ); + //Workaround for PHP bug https://bugs.php.net/bug.php?id=69197 + if ( empty( $this->sign_extracerts_file ) ) { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath( $this->sign_cert_file ), + array( 'file://' . realpath( $this->sign_key_file ), $this->sign_key_pass ), + null + ); + } else { + $sign = @openssl_pkcs7_sign( + $file, + $signed, + 'file://' . realpath( $this->sign_cert_file ), + array( 'file://' . realpath( $this->sign_key_file ), $this->sign_key_pass ), + null, + PKCS7_DETACHED, + $this->sign_extracerts_file + ); + } + if ( $sign ) { + @unlink( $file ); + $body = file_get_contents( $signed ); + @unlink( $signed ); + //The message returned by openssl contains both headers and body, so need to split them up + $parts = explode( "\n\n", $body, 2 ); + $this->MIMEHeader .= $parts[0] . $this->LE . $this->LE; + $body = $parts[1]; + } else { + @unlink( $file ); + @unlink( $signed ); + throw new phpmailerException( $this->lang( 'signing' ) . openssl_error_string() ); + } + } catch ( phpmailerException $exc ) { + $body = ''; + if ( $this->exceptions ) { + throw $exc; + } + } + } + + return $body; + } + + /** + * Get the message MIME type headers. + * @access public + * @return string + */ + public function getMailMIME() { + $result = ''; + $ismultipart = true; + switch ( $this->message_type ) { + case 'inline': + $result .= $this->headerLine( 'Content-Type', 'multipart/related;' ); + $result .= $this->textLine( "\tboundary=\"" . $this->boundary[1] . '"' ); + break; + case 'attach': + case 'inline_attach': + case 'alt_attach': + case 'alt_inline_attach': + $result .= $this->headerLine( 'Content-Type', 'multipart/mixed;' ); + $result .= $this->textLine( "\tboundary=\"" . $this->boundary[1] . '"' ); + break; + case 'alt': + case 'alt_inline': + $result .= $this->headerLine( 'Content-Type', 'multipart/alternative;' ); + $result .= $this->textLine( "\tboundary=\"" . $this->boundary[1] . '"' ); + break; + default: + // Catches case 'plain': and case '': + $result .= $this->textLine( 'Content-Type: ' . $this->ContentType . '; charset=' . $this->CharSet ); + $ismultipart = false; + break; + } + // RFC1341 part 5 says 7bit is assumed if not specified + if ( $this->Encoding != '7bit' ) { + // RFC 2045 section 6.4 says multipart MIME parts may only use 7bit, 8bit or binary CTE + if ( $ismultipart ) { + if ( $this->Encoding == '8bit' ) { + $result .= $this->headerLine( 'Content-Transfer-Encoding', '8bit' ); + } + // The only remaining alternatives are quoted-printable and base64, which are both 7bit compatible + } else { + $result .= $this->headerLine( 'Content-Transfer-Encoding', $this->Encoding ); + } + } + + if ( $this->Mailer != 'mail' ) { + $result .= $this->LE; + } + + return $result; + } + + /** + * Format a header line. + * @access public + * + * @param string $name + * @param string $value + * + * @return string + */ + public function headerLine( $name, $value ) { + return $name . ': ' . $value . $this->LE; + } + + /** + * Return a formatted mail line. + * @access public + * + * @param string $value + * + * @return string + */ + public function textLine( $value ) { + return $value . $this->LE; + } + + /** + * Apply word wrapping to the message body. + * Wraps the message body to the number of chars set in the WordWrap property. + * You should only do this to plain-text bodies as wrapping HTML tags may break them. + * This is called automatically by createBody(), so you don't need to call it yourself. + * @access public + * @return void + */ + public function setWordWrap() { + if ( $this->WordWrap < 1 ) { + return; + } + + switch ( $this->message_type ) { + case 'alt': + case 'alt_inline': + case 'alt_attach': + case 'alt_inline_attach': + $this->AltBody = $this->wrapText( $this->AltBody, $this->WordWrap ); + break; + default: + $this->Body = $this->wrapText( $this->Body, $this->WordWrap ); + break; + } + } + + /** + * Detect if a string contains a line longer than the maximum line length allowed. + * + * @param string $str + * + * @return boolean + * @static + */ + public static function hasLineLongerThanMax( $str ) { + //+2 to include CRLF line break for a 1000 total + return (boolean) preg_match( '/^(.{' . ( self::MAX_LINE_LENGTH + 2 ) . ',})/m', $str ); + } + + /** + * Return the start of a message boundary. + * @access protected + * + * @param string $boundary + * @param string $charSet + * @param string $contentType + * @param string $encoding + * + * @return string + */ + protected function getBoundary( $boundary, $charSet, $contentType, $encoding ) { + $result = ''; + if ( $charSet == '' ) { + $charSet = $this->CharSet; + } + if ( $contentType == '' ) { + $contentType = $this->ContentType; + } + if ( $encoding == '' ) { + $encoding = $this->Encoding; + } + $result .= $this->textLine( '--' . $boundary ); + $result .= sprintf( 'Content-Type: %s; charset=%s', $contentType, $charSet ); + $result .= $this->LE; + // RFC1341 part 5 says 7bit is assumed if not specified + if ( $encoding != '7bit' ) { + $result .= $this->headerLine( 'Content-Transfer-Encoding', $encoding ); + } + $result .= $this->LE; + + return $result; + } + + /** + * Encode a string in requested format. + * Returns an empty string on failure. + * + * @param string $str The text to encode + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @access public + * @return string + */ + public function encodeString( $str, $encoding = 'base64' ) { + $encoded = ''; + switch ( strtolower( $encoding ) ) { + case 'base64': + $encoded = chunk_split( base64_encode( $str ), 76, $this->LE ); + break; + case '7bit': + case '8bit': + $encoded = $this->fixEOL( $str ); + // Make sure it ends with a line break + if ( substr( $encoded, - ( strlen( $this->LE ) ) ) != $this->LE ) { + $encoded .= $this->LE; + } + break; + case 'binary': + $encoded = $str; + break; + case 'quoted-printable': + $encoded = $this->encodeQP( $str ); + break; + default: + $this->setError( $this->lang( 'encoding' ) . $encoding ); + break; + } + + return $encoded; + } + + /** + * Encode a string in quoted-printable format. + * According to RFC2045 section 6.7. + * @access public + * + * @param string $string The text to encode + * @param integer $line_max Number of chars allowed on a line before wrapping + * + * @return string + * @link http://www.php.net/manual/en/function.quoted-printable-decode.php#89417 Adapted from this comment + */ + public function encodeQP( $string, $line_max = 76 ) { + // Use native function if it's available (>= PHP5.3) + if ( function_exists( 'quoted_printable_encode' ) ) { + return quoted_printable_encode( $string ); + } + // Fall back to a pure PHP implementation + $string = str_replace( + array( '%20', '%0D%0A.', '%0D%0A', '%' ), + array( ' ', "\r\n=2E", "\r\n", '=' ), + rawurlencode( $string ) + ); + + return preg_replace( '/[^\r\n]{' . ( $line_max - 3 ) . '}[^=\r\n]{2}/', "$0=\r\n", $string ); + } + + /** + * Attach all file, string, and binary attachments to the message. + * Returns an empty string on failure. + * @access protected + * + * @param string $disposition_type + * @param string $boundary + * + * @return string + */ + protected function attachAll( $disposition_type, $boundary ) { + // Return text of body + $mime = array(); + $cidUniq = array(); + $incl = array(); + + // Add all attachments + foreach ( $this->attachment as $attachment ) { + // Check if it is a valid disposition_filter + if ( $attachment[6] == $disposition_type ) { + // Check for string attachment + $string = ''; + $path = ''; + $bString = $attachment[5]; + if ( $bString ) { + $string = $attachment[0]; + } else { + $path = $attachment[0]; + } + + $inclhash = md5( serialize( $attachment ) ); + if ( in_array( $inclhash, $incl ) ) { + continue; + } + $incl[] = $inclhash; + $name = $attachment[2]; + $encoding = $attachment[3]; + $type = $attachment[4]; + $disposition = $attachment[6]; + $cid = $attachment[7]; + if ( $disposition == 'inline' && array_key_exists( $cid, $cidUniq ) ) { + continue; + } + $cidUniq[ $cid ] = true; + + $mime[] = sprintf( '--%s%s', $boundary, $this->LE ); + //Only include a filename property if we have one + if ( ! empty( $name ) ) { + $mime[] = sprintf( + 'Content-Type: %s; name="%s"%s', + $type, + $this->encodeHeader( $this->secureHeader( $name ) ), + $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Type: %s%s', + $type, + $this->LE + ); + } + // RFC1341 part 5 says 7bit is assumed if not specified + if ( $encoding != '7bit' ) { + $mime[] = sprintf( 'Content-Transfer-Encoding: %s%s', $encoding, $this->LE ); + } + + if ( $disposition == 'inline' ) { + $mime[] = sprintf( 'Content-ID: <%s>%s', $cid, $this->LE ); + } + + // If a filename contains any of these chars, it should be quoted, + // but not otherwise: RFC2183 & RFC2045 5.1 + // Fixes a warning in IETF's msglint MIME checker + // Allow for bypassing the Content-Disposition header totally + if ( ! ( empty( $disposition ) ) ) { + $encoded_name = $this->encodeHeader( $this->secureHeader( $name ) ); + if ( preg_match( '/[ \(\)<>@,;:\\"\/\[\]\?=]/', $encoded_name ) ) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename="%s"%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + if ( ! empty( $encoded_name ) ) { + $mime[] = sprintf( + 'Content-Disposition: %s; filename=%s%s', + $disposition, + $encoded_name, + $this->LE . $this->LE + ); + } else { + $mime[] = sprintf( + 'Content-Disposition: %s%s', + $disposition, + $this->LE . $this->LE + ); + } + } + } else { + $mime[] = $this->LE; + } + + // Encode as string attachment + if ( $bString ) { + $mime[] = $this->encodeString( $string, $encoding ); + if ( $this->isError() ) { + return ''; + } + $mime[] = $this->LE . $this->LE; + } else { + $mime[] = $this->encodeFile( $path, $encoding ); + if ( $this->isError() ) { + return ''; + } + $mime[] = $this->LE . $this->LE; + } + } + } + + $mime[] = sprintf( '--%s--%s', $boundary, $this->LE ); + + return implode( '', $mime ); + } + + /** + * Check if an error occurred. + * @access public + * @return boolean True if an error did occur. + */ + public function isError() { + return ( $this->error_count > 0 ); + } + + /** + * Encode a file attachment in requested format. + * Returns an empty string on failure. + * + * @param string $path The full path to the file + * @param string $encoding The encoding to use; one of 'base64', '7bit', '8bit', 'binary', 'quoted-printable' + * + * @throws phpmailerException + * @access protected + * @return string + */ + protected function encodeFile( $path, $encoding = 'base64' ) { + try { + if ( ! is_readable( $path ) ) { + throw new phpmailerException( $this->lang( 'file_open' ) . $path, self::STOP_CONTINUE ); + } + $magic_quotes = get_magic_quotes_runtime(); + if ( $magic_quotes ) { + if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) { + set_magic_quotes_runtime( false ); + } else { + //Doesn't exist in PHP 5.4, but we don't need to check because + //get_magic_quotes_runtime always returns false in 5.4+ + //so it will never get here + ini_set( 'magic_quotes_runtime', false ); + } + } + $file_buffer = file_get_contents( $path ); + $file_buffer = $this->encodeString( $file_buffer, $encoding ); + if ( $magic_quotes ) { + if ( version_compare( PHP_VERSION, '5.3.0', '<' ) ) { + set_magic_quotes_runtime( $magic_quotes ); + } else { + ini_set( 'magic_quotes_runtime', $magic_quotes ); + } + } + + return $file_buffer; + } catch ( Exception $exc ) { + $this->setError( $exc->getMessage() ); + + return ''; + } + } + + /** + * Return the end of a message boundary. + * @access protected + * + * @param string $boundary + * + * @return string + */ + protected function endBoundary( $boundary ) { + return $this->LE . '--' . $boundary . '--' . $this->LE; + } + + /** + * Assemble message headers. + * @access public + * @return string The assembled headers + */ + public function createHeader() { + $result = ''; + + if ( $this->MessageDate == '' ) { + $this->MessageDate = self::rfcDate(); + } + $result .= $this->headerLine( 'Date', $this->MessageDate ); + + // To be created automatically by mail() + if ( $this->SingleTo ) { + if ( $this->Mailer != 'mail' ) { + foreach ( $this->to as $toaddr ) { + $this->SingleToArray[] = $this->addrFormat( $toaddr ); + } + } + } else { + if ( count( $this->to ) > 0 ) { + if ( $this->Mailer != 'mail' ) { + $result .= $this->addrAppend( 'To', $this->to ); + } + } elseif ( count( $this->cc ) == 0 ) { + $result .= $this->headerLine( 'To', 'undisclosed-recipients:;' ); + } + } + + $result .= $this->addrAppend( 'From', array( array( trim( $this->From ), $this->FromName ) ) ); + + // sendmail and mail() extract Cc from the header before sending + if ( count( $this->cc ) > 0 ) { + $result .= $this->addrAppend( 'Cc', $this->cc ); + } + + // sendmail and mail() extract Bcc from the header before sending + if ( ( + $this->Mailer == 'sendmail' or $this->Mailer == 'qmail' or $this->Mailer == 'mail' + ) + and count( $this->bcc ) > 0 + ) { + $result .= $this->addrAppend( 'Bcc', $this->bcc ); + } + + if ( count( $this->ReplyTo ) > 0 ) { + $result .= $this->addrAppend( 'Reply-To', $this->ReplyTo ); + } + + // mail() sets the subject itself + if ( $this->Mailer != 'mail' ) { + $result .= $this->headerLine( 'Subject', $this->encodeHeader( $this->secureHeader( $this->Subject ) ) ); + } + + if ( '' != $this->MessageID and preg_match( '/^<.*@.*>$/', $this->MessageID ) ) { + $this->lastMessageID = $this->MessageID; + } else { + $this->lastMessageID = sprintf( '<%s@%s>', $this->uniqueid, $this->serverHostname() ); + } + $result .= $this->headerLine( 'Message-ID', $this->lastMessageID ); + if ( ! is_null( $this->Priority ) ) { + $result .= $this->headerLine( 'X-Priority', $this->Priority ); + } + if ( $this->XMailer == '' ) { + $result .= $this->headerLine( + 'X-Mailer', + 'PHPMailer ' . $this->Version . ' (https://github.com/PHPMailer/PHPMailer)' + ); + } else { + $myXmailer = trim( $this->XMailer ); + if ( $myXmailer ) { + $result .= $this->headerLine( 'X-Mailer', $myXmailer ); + } + } + + if ( $this->ConfirmReadingTo != '' ) { + $result .= $this->headerLine( 'Disposition-Notification-To', '<' . $this->ConfirmReadingTo . '>' ); + } + + // Add custom headers + foreach ( $this->CustomHeader as $header ) { + $result .= $this->headerLine( + trim( $header[0] ), + $this->encodeHeader( trim( $header[1] ) ) + ); + } + if ( ! $this->sign_key_file ) { + $result .= $this->headerLine( 'MIME-Version', '1.0' ); + $result .= $this->getMailMIME(); + } + + return $result; + } + + /** + * Return an RFC 822 formatted date. + * @access public + * @return string + * @static + */ + public static function rfcDate() { + // Set the time zone to whatever the default is to avoid 500 errors + // Will default to UTC if it's not set properly in php.ini + date_default_timezone_set( @date_default_timezone_get() ); + + return date( 'D, j M Y H:i:s O' ); + } + + /** + * Format an address for use in a message header. + * @access public + * + * @param array $addr A 2-element indexed array, element 0 containing an address, element 1 containing a name + * like array('joe@example.com', 'Joe User') + * + * @return string + */ + public function addrFormat( $addr ) { + if ( empty( $addr[1] ) ) { // No name provided + return $this->secureHeader( $addr[0] ); + } else { + return $this->encodeHeader( $this->secureHeader( $addr[1] ), 'phrase' ) . ' <' . $this->secureHeader( + $addr[0] + ) . '>'; + } + } + + /** + * Create recipient headers. + * @access public + * + * @param string $type + * @param array $addr An array of recipient, + * where each recipient is a 2-element indexed array with element 0 containing an address + * and element 1 containing a name, like: + * array(array('joe@example.com', 'Joe User'), array('zoe@example.com', 'Zoe User')) + * + * @return string + */ + public function addrAppend( $type, $addr ) { + $addresses = array(); + foreach ( $addr as $address ) { + $addresses[] = $this->addrFormat( $address ); + } + + return $type . ': ' . implode( ', ', $addresses ) . $this->LE; + } + + /** + * Get the server hostname. + * Returns 'localhost.localdomain' if unknown. + * @access protected + * @return string + */ + protected function serverHostname() { + $result = 'localhost.localdomain'; + if ( ! empty( $this->Hostname ) ) { + $result = $this->Hostname; + } elseif ( isset( $_SERVER ) and array_key_exists( 'SERVER_NAME', $_SERVER ) and ! empty( $_SERVER['SERVER_NAME'] ) ) { + $result = $_SERVER['SERVER_NAME']; + } elseif ( function_exists( 'gethostname' ) && gethostname() !== false ) { + $result = gethostname(); + } elseif ( php_uname( 'n' ) !== false ) { + $result = php_uname( 'n' ); + } + + return $result; + } + + /** + * Create the DKIM header and body in a new message header. + * @access public + * + * @param string $headers_line Header lines + * @param string $subject Subject + * @param string $body Body + * + * @return string + */ + public function DKIM_Add( $headers_line, $subject, $body ) { + $DKIMsignatureType = 'rsa-sha256'; // Signature & hash algorithms + $DKIMcanonicalization = 'relaxed/simple'; // Canonicalization of header/body + $DKIMquery = 'dns/txt'; // Query method + $DKIMtime = time(); // Signature Timestamp = seconds since 00:00:00 - Jan 1, 1970 (UTC time zone) + $subject_header = "Subject: $subject"; + $headers = explode( $this->LE, $headers_line ); + $from_header = ''; + $to_header = ''; + $date_header = ''; + $current = ''; + foreach ( $headers as $header ) { + if ( strpos( $header, 'From:' ) === 0 ) { + $from_header = $header; + $current = 'from_header'; + } elseif ( strpos( $header, 'To:' ) === 0 ) { + $to_header = $header; + $current = 'to_header'; + } elseif ( strpos( $header, 'Date:' ) === 0 ) { + $date_header = $header; + $current = 'date_header'; + } else { + if ( ! empty( $$current ) && strpos( $header, ' =?' ) === 0 ) { + $$current .= $header; + } else { + $current = ''; + } + } + } + $from = str_replace( '|', '=7C', $this->DKIM_QP( $from_header ) ); + $to = str_replace( '|', '=7C', $this->DKIM_QP( $to_header ) ); + $date = str_replace( '|', '=7C', $this->DKIM_QP( $date_header ) ); + $subject = str_replace( + '|', + '=7C', + $this->DKIM_QP( $subject_header ) + ); // Copied header fields (dkim-quoted-printable) + $body = $this->DKIM_BodyC( $body ); + $DKIMlen = strlen( $body ); // Length of body + $DKIMb64 = base64_encode( pack( 'H*', hash( 'sha256', $body ) ) ); // Base64 of packed binary SHA-256 hash of body + if ( '' == $this->DKIM_identity ) { + $ident = ''; + } else { + $ident = ' i=' . $this->DKIM_identity . ';'; + } + $dkimhdrs = 'DKIM-Signature: v=1; a=' . + $DKIMsignatureType . '; q=' . + $DKIMquery . '; l=' . + $DKIMlen . '; s=' . + $this->DKIM_selector . + ";\r\n" . + "\tt=" . $DKIMtime . '; c=' . $DKIMcanonicalization . ";\r\n" . + "\th=From:To:Date:Subject;\r\n" . + "\td=" . $this->DKIM_domain . ';' . $ident . "\r\n" . + "\tz=$from\r\n" . + "\t|$to\r\n" . + "\t|$date\r\n" . + "\t|$subject;\r\n" . + "\tbh=" . $DKIMb64 . ";\r\n" . + "\tb="; + $toSign = $this->DKIM_HeaderC( + $from_header . "\r\n" . + $to_header . "\r\n" . + $date_header . "\r\n" . + $subject_header . "\r\n" . + $dkimhdrs + ); + $signed = $this->DKIM_Sign( $toSign ); + + return $dkimhdrs . $signed . "\r\n"; + } + + /** + * Quoted-Printable-encode a DKIM header. + * @access public + * + * @param string $txt + * + * @return string + */ + public function DKIM_QP( $txt ) { + $line = ''; + for ( $i = 0; $i < strlen( $txt ); $i ++ ) { + $ord = ord( $txt[ $i ] ); + if ( ( ( 0x21 <= $ord ) && ( $ord <= 0x3A ) ) || $ord == 0x3C || ( ( 0x3E <= $ord ) && ( $ord <= 0x7E ) ) ) { + $line .= $txt[ $i ]; + } else { + $line .= '=' . sprintf( '%02X', $ord ); + } + } + + return $line; + } + + /** + * Generate a DKIM canonicalization body. + * @access public + * + * @param string $body Message Body + * + * @return string + */ + public function DKIM_BodyC( $body ) { + if ( $body == '' ) { + return "\r\n"; + } + // stabilize line endings + $body = str_replace( "\r\n", "\n", $body ); + $body = str_replace( "\n", "\r\n", $body ); + // END stabilize line endings + while ( substr( $body, strlen( $body ) - 4, 4 ) == "\r\n\r\n" ) { + $body = substr( $body, 0, strlen( $body ) - 2 ); + } + + return $body; + } + + /** + * Generate a DKIM canonicalization header. + * @access public + * + * @param string $signHeader Header + * + * @return string + */ + public function DKIM_HeaderC( $signHeader ) { + $signHeader = preg_replace( '/\r\n\s+/', ' ', $signHeader ); + $lines = explode( "\r\n", $signHeader ); + foreach ( $lines as $key => $line ) { + list( $heading, $value ) = explode( ':', $line, 2 ); + $heading = strtolower( $heading ); + $value = preg_replace( '/\s{2,}/', ' ', $value ); // Compress useless spaces + $lines[ $key ] = $heading . ':' . trim( $value ); // Don't forget to remove WSP around the value + } + $signHeader = implode( "\r\n", $lines ); + + return $signHeader; + } + + /** + * Generate a DKIM signature. + * @access public + * + * @param string $signHeader + * + * @throws phpmailerException + * @return string + */ + public function DKIM_Sign( $signHeader ) { + if ( ! defined( 'PKCS7_TEXT' ) ) { + if ( $this->exceptions ) { + throw new phpmailerException( $this->lang( 'extension_missing' ) . 'openssl' ); + } + + return ''; + } + $privKeyStr = file_get_contents( $this->DKIM_private ); + if ( $this->DKIM_passphrase != '' ) { + $privKey = openssl_pkey_get_private( $privKeyStr, $this->DKIM_passphrase ); + } else { + $privKey = openssl_pkey_get_private( $privKeyStr ); + } + if ( openssl_sign( $signHeader, $signature, $privKey, 'sha256WithRSAEncryption' ) ) { //sha1WithRSAEncryption + openssl_pkey_free( $privKey ); + + return base64_encode( $signature ); + } + openssl_pkey_free( $privKey ); + + return ''; + } + + /** + * Actually send a message. + * Send the email via the selected mechanism + * @throws phpmailerException + * @return boolean + */ + public function postSend() { + try { + // Choose the mailer and send through it + switch ( $this->Mailer ) { + case 'sendmail': + case 'qmail': + return $this->sendmailSend( $this->MIMEHeader, $this->MIMEBody ); + case 'smtp': + return $this->smtpSend( $this->MIMEHeader, $this->MIMEBody ); + case 'mail': + return $this->mailSend( $this->MIMEHeader, $this->MIMEBody ); + default: + $sendMethod = $this->Mailer . 'Send'; + if ( method_exists( $this, $sendMethod ) ) { + return $this->$sendMethod( $this->MIMEHeader, $this->MIMEBody ); + } + + return $this->mailSend( $this->MIMEHeader, $this->MIMEBody ); + } + } catch ( phpmailerException $exc ) { + $this->setError( $exc->getMessage() ); + $this->edebug( $exc->getMessage() ); + if ( $this->exceptions ) { + throw $exc; + } + } + + return false; + } + + /** + * Send mail using the $Sendmail program. + * + * @param string $header The message headers + * @param string $body The message body + * + * @see PHPMailer::$Sendmail + * @throws phpmailerException + * @access protected + * @return boolean + */ + protected function sendmailSend( $header, $body ) { + if ( $this->Sender != '' ) { + if ( $this->Mailer == 'qmail' ) { + $sendmail = sprintf( '%s -f%s', escapeshellcmd( $this->Sendmail ), escapeshellarg( $this->Sender ) ); + } else { + $sendmail = sprintf( '%s -oi -f%s -t', escapeshellcmd( $this->Sendmail ), escapeshellarg( $this->Sender ) ); + } + } else { + if ( $this->Mailer == 'qmail' ) { + $sendmail = sprintf( '%s', escapeshellcmd( $this->Sendmail ) ); + } else { + $sendmail = sprintf( '%s -oi -t', escapeshellcmd( $this->Sendmail ) ); + } + } + if ( $this->SingleTo ) { + foreach ( $this->SingleToArray as $toAddr ) { + if ( ! @$mail = popen( $sendmail, 'w' ) ) { + throw new phpmailerException( $this->lang( 'execute' ) . $this->Sendmail, self::STOP_CRITICAL ); + } + fputs( $mail, 'To: ' . $toAddr . "\n" ); + fputs( $mail, $header ); + fputs( $mail, $body ); + $result = pclose( $mail ); + $this->doCallback( + ( $result == 0 ), + array( $toAddr ), + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); + if ( $result != 0 ) { + throw new phpmailerException( $this->lang( 'execute' ) . $this->Sendmail, self::STOP_CRITICAL ); + } + } + } else { + if ( ! @$mail = popen( $sendmail, 'w' ) ) { + throw new phpmailerException( $this->lang( 'execute' ) . $this->Sendmail, self::STOP_CRITICAL ); + } + fputs( $mail, $header ); + fputs( $mail, $body ); + $result = pclose( $mail ); + $this->doCallback( + ( $result == 0 ), + $this->to, + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From + ); + if ( $result != 0 ) { + throw new phpmailerException( $this->lang( 'execute' ) . $this->Sendmail, self::STOP_CRITICAL ); + } + } + + return true; + } + + /** + * Perform a callback. + * + * @param boolean $isSent + * @param array $to + * @param array $cc + * @param array $bcc + * @param string $subject + * @param string $body + * @param string $from + */ + protected function doCallback( $isSent, $to, $cc, $bcc, $subject, $body, $from ) { + if ( ! empty( $this->action_function ) && is_callable( $this->action_function ) ) { + $params = array( $isSent, $to, $cc, $bcc, $subject, $body, $from ); + call_user_func_array( $this->action_function, $params ); + } + } + + /** + * Send mail via SMTP. + * Returns false if there is a bad MAIL FROM, RCPT, or DATA input. + * Uses the PHPMailerSMTP class by default. + * @see PHPMailer::getSMTPInstance() to use a different class. + * + * @param string $header The message headers + * @param string $body The message body + * + * @throws phpmailerException + * @uses SMTP + * @access protected + * @return boolean + */ + protected function smtpSend( $header, $body ) { + $bad_rcpt = array(); + if ( ! $this->smtpConnect( $this->SMTPOptions ) ) { + throw new phpmailerException( $this->lang( 'smtp_connect_failed' ), self::STOP_CRITICAL ); + } + if ( '' == $this->Sender ) { + $smtp_from = $this->From; + } else { + $smtp_from = $this->Sender; + } + if ( ! $this->smtp->mail( $smtp_from ) ) { + $this->setError( $this->lang( 'from_failed' ) . $smtp_from . ' : ' . implode( ',', $this->smtp->getError() ) ); + throw new phpmailerException( $this->ErrorInfo, self::STOP_CRITICAL ); + } + + // Attempt to send to all recipients + foreach ( array( $this->to, $this->cc, $this->bcc ) as $togroup ) { + foreach ( $togroup as $to ) { + if ( ! $this->smtp->recipient( $to[0] ) ) { + $error = $this->smtp->getError(); + $bad_rcpt[] = array( 'to' => $to[0], 'error' => $error['detail'] ); + $isSent = false; + } else { + $isSent = true; + } + $this->doCallback( $isSent, array( $to[0] ), array(), array(), $this->Subject, $body, $this->From ); + } + } + + // Only send the DATA command if we have viable recipients + if ( ( count( $this->all_recipients ) > count( $bad_rcpt ) ) and ! $this->smtp->data( $header . $body ) ) { + throw new phpmailerException( $this->lang( 'data_not_accepted' ), self::STOP_CRITICAL ); + } + if ( $this->SMTPKeepAlive ) { + $this->smtp->reset(); + } else { + $this->smtp->quit(); + $this->smtp->close(); + } + //Create error message for any bad addresses + if ( count( $bad_rcpt ) > 0 ) { + $errstr = ''; + foreach ( $bad_rcpt as $bad ) { + $errstr .= $bad['to'] . ': ' . $bad['error']; + } + throw new phpmailerException( + $this->lang( 'recipients_failed' ) . $errstr, + self::STOP_CONTINUE + ); + } + + return true; + } + + /** + * Initiate a connection to an SMTP server. + * Returns false if the operation failed. + * + * @param array $options An array of options compatible with stream_context_create() + * + * @return bool + * @throws null + * @throws phpmailerException + * @uses SMTP + * @access public + */ + public function smtpConnect( $options = array() ) { + if ( is_null( $this->smtp ) ) { + $this->smtp = $this->getSMTPInstance(); + } + + // Already connected? + if ( $this->smtp->connected() ) { + return true; + } + + $this->smtp->setTimeout( $this->Timeout ); + $this->smtp->setDebugLevel( $this->SMTPDebug ); + $this->smtp->setDebugOutput( $this->Debugoutput ); + $this->smtp->setVerp( $this->do_verp ); + $hosts = explode( ';', $this->Host ); + $lastexception = null; + + foreach ( $hosts as $hostentry ) { + $hostinfo = array(); + if ( ! preg_match( '/^((ssl|tls):\/\/)*([a-zA-Z0-9\.-]*):?([0-9]*)$/', trim( $hostentry ), $hostinfo ) ) { + // Not a valid host entry + continue; + } + // $hostinfo[2]: optional ssl or tls prefix + // $hostinfo[3]: the hostname + // $hostinfo[4]: optional port number + // The host string prefix can temporarily override the current setting for SMTPSecure + // If it's not specified, the default value is used + $prefix = ''; + $secure = $this->SMTPSecure; + $tls = ( $this->SMTPSecure == 'tls' ); + if ( 'ssl' == $hostinfo[2] or ( '' == $hostinfo[2] and 'ssl' == $this->SMTPSecure ) ) { + $prefix = 'ssl://'; + $tls = false; // Can't have SSL and TLS at the same time + $secure = 'ssl'; + } elseif ( $hostinfo[2] == 'tls' ) { + $tls = true; + // tls doesn't use a prefix + $secure = 'tls'; + } + //Do we need the OpenSSL extension? + $sslext = defined( 'OPENSSL_ALGO_SHA1' ); + if ( 'tls' === $secure or 'ssl' === $secure ) { + //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled + if ( ! $sslext ) { + throw new phpmailerException( $this->lang( 'extension_missing' ) . 'openssl', self::STOP_CRITICAL ); + } + } + $host = $hostinfo[3]; + $port = $this->Port; + $tport = (integer) $hostinfo[4]; + if ( $tport > 0 and $tport < 65536 ) { + $port = $tport; + } + if ( $this->smtp->connect( $prefix . $host, $port, $this->Timeout, $options ) ) { + try { + if ( $this->Helo ) { + $hello = $this->Helo; + } else { + $hello = $this->serverHostname(); + } + $this->smtp->hello( $hello ); + //Automatically enable TLS encryption if: + // * it's not disabled + // * we have openssl extension + // * we are not already using SSL + // * the server offers STARTTLS + if ( $this->SMTPAutoTLS and $sslext and $secure != 'ssl' and $this->smtp->getServerExt( 'STARTTLS' ) ) { + $tls = true; + } + if ( $tls ) { + if ( ! $this->smtp->startTLS() ) { + throw new phpmailerException( $this->lang( 'connect_host' ) ); + } + // We must resend HELO after tls negotiation + $this->smtp->hello( $hello ); + } + if ( $this->SMTPAuth ) { + if ( ! $this->smtp->authenticate( + $this->Username, + $this->Password, + $this->AuthType, + $this->Realm, + $this->Workstation + ) + ) { + throw new phpmailerException( $this->lang( 'authenticate' ) ); + } + } + + return true; + } catch ( phpmailerException $exc ) { + $lastexception = $exc; + $this->edebug( $exc->getMessage() ); + // We must have connected, but then failed TLS or Auth, so close connection nicely + $this->smtp->quit(); + } + } + } + // If we get here, all connection attempts have failed, so close connection hard + $this->smtp->close(); + // As we've caught all exceptions, just report whatever the last one was + if ( $this->exceptions and ! is_null( $lastexception ) ) { + throw $lastexception; + } + + return false; + } + + /** + * Get an instance to use for SMTP operations. + * Override this function to load your own SMTP implementation + * @return SMTP + */ + public function getSMTPInstance() { + if ( ! is_object( $this->smtp ) ) { + $this->smtp = new SMTP; + } + + return $this->smtp; + } + + /** + * Send mail using the PHP mail() function. + * + * @param string $header The message headers + * @param string $body The message body + * + * @link http://www.php.net/manual/en/book.mail.php + * @throws phpmailerException + * @access protected + * @return boolean + */ + protected function mailSend( $header, $body ) { + $toArr = array(); + foreach ( $this->to as $toaddr ) { + $toArr[] = $this->addrFormat( $toaddr ); + } + $to = implode( ', ', $toArr ); + + if ( empty( $this->Sender ) ) { + $params = ' '; + } else { + $params = sprintf( '-f%s', $this->Sender ); + } + if ( $this->Sender != '' and ! ini_get( 'safe_mode' ) ) { + $old_from = ini_get( 'sendmail_from' ); + ini_set( 'sendmail_from', $this->Sender ); + } + $result = false; + if ( $this->SingleTo && count( $toArr ) > 1 ) { + foreach ( $toArr as $toAddr ) { + $result = $this->mailPassthru( $toAddr, $this->Subject, $body, $header, $params ); + $this->doCallback( $result, array( $toAddr ), $this->cc, $this->bcc, $this->Subject, $body, $this->From ); + } + } else { + $result = $this->mailPassthru( $to, $this->Subject, $body, $header, $params ); + $this->doCallback( $result, $this->to, $this->cc, $this->bcc, $this->Subject, $body, $this->From ); + } + if ( isset( $old_from ) ) { + ini_set( 'sendmail_from', $old_from ); + } + if ( ! $result ) { + throw new phpmailerException( $this->lang( 'instantiate' ), self::STOP_CRITICAL ); + } + + return true; + } + + /** + * Call mail() in a safe_mode-aware fashion. + * Also, unless sendmail_path points to sendmail (or something that + * claims to be sendmail), don't pass params (not a perfect fix, + * but it will do) + * + * @param string $to To + * @param string $subject Subject + * @param string $body Message Body + * @param string $header Additional Header(s) + * @param string $params Params + * + * @access private + * @return boolean + */ + private function mailPassthru( $to, $subject, $body, $header, $params ) { + //Check overloading of mail function to avoid double-encoding + if ( ini_get( 'mbstring.func_overload' ) & 1 ) { + $subject = $this->secureHeader( $subject ); + } else { + $subject = $this->encodeHeader( $this->secureHeader( $subject ) ); + } + if ( ini_get( 'safe_mode' ) || ! ( $this->UseSendmailOptions ) ) { + $result = @mail( $to, $subject, $body, $header ); + } else { + $result = @mail( $to, $subject, $body, $header, $params ); + } + + return $result; + } + + /** + * Strip newlines to prevent header injection. + * @access public + * + * @param string $str + * + * @return string + */ + public function secureHeader( $str ) { + return trim( str_replace( array( "\r", "\n" ), '', $str ) ); + } + + /** + * Encode a header string optimally. + * Picks shortest of Q, B, quoted-printable or none. + * @access public + * + * @param string $str + * @param string $position + * + * @return string + */ + public function encodeHeader( $str, $position = 'text' ) { + $matchcount = 0; + switch ( strtolower( $position ) ) { + case 'phrase': + if ( ! preg_match( '/[\200-\377]/', $str ) ) { + // Can't use addslashes as we don't know the value of magic_quotes_sybase + $encoded = addcslashes( $str, "\0..\37\177\\\"" ); + if ( ( $str == $encoded ) && ! preg_match( '/[^A-Za-z0-9!#$%&\'*+\/=?^_`{|}~ -]/', $str ) ) { + return ( $encoded ); + } else { + return ( "\"$encoded\"" ); + } + } + $matchcount = preg_match_all( '/[^\040\041\043-\133\135-\176]/', $str, $matches ); + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + $matchcount = preg_match_all( '/[()"]/', $str, $matches ); + // Intentional fall-through + case 'text': + default: + $matchcount += preg_match_all( '/[\000-\010\013\014\016-\037\177-\377]/', $str, $matches ); + break; + } + + //There are no chars that need encoding + if ( $matchcount == 0 ) { + return ( $str ); + } + + $maxlen = 75 - 7 - strlen( $this->CharSet ); + // Try to select the encoding which should produce the shortest output + if ( $matchcount > strlen( $str ) / 3 ) { + // More than a third of the content will need encoding, so B encoding will be most efficient + $encoding = 'B'; + if ( function_exists( 'mb_strlen' ) && $this->hasMultiBytes( $str ) ) { + // Use a custom function which correctly encodes and wraps long + // multibyte strings without breaking lines within a character + $encoded = $this->base64EncodeWrapMB( $str, "\n" ); + } else { + $encoded = base64_encode( $str ); + $maxlen -= $maxlen % 4; + $encoded = trim( chunk_split( $encoded, $maxlen, "\n" ) ); + } + } else { + $encoding = 'Q'; + $encoded = $this->encodeQ( $str, $position ); + $encoded = $this->wrapText( $encoded, $maxlen, true ); + $encoded = str_replace( '=' . self::CRLF, "\n", trim( $encoded ) ); + } + + $encoded = preg_replace( '/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded ); + $encoded = trim( str_replace( "\n", $this->LE, $encoded ) ); + + return $encoded; + } + + /** + * Check if a string contains multi-byte characters. + * @access public + * + * @param string $str multi-byte text to wrap encode + * + * @return boolean + */ + public function hasMultiBytes( $str ) { + if ( function_exists( 'mb_strlen' ) ) { + return ( strlen( $str ) > mb_strlen( $str, $this->CharSet ) ); + } else { // Assume no multibytes (we can't handle without mbstring functions anyway) + return false; + } + } + + /** + * Encode and wrap long multibyte strings for mail headers + * without breaking lines within a character. + * Adapted from a function by paravoid + * @link http://www.php.net/manual/en/function.mb-encode-mimeheader.php#60283 + * @access public + * + * @param string $str multi-byte text to wrap encode + * @param string $linebreak string to use as linefeed/end-of-line + * + * @return string + */ + public function base64EncodeWrapMB( $str, $linebreak = null ) { + $start = '=?' . $this->CharSet . '?B?'; + $end = '?='; + $encoded = ''; + if ( $linebreak === null ) { + $linebreak = $this->LE; + } + + $mb_length = mb_strlen( $str, $this->CharSet ); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen( $start ) - strlen( $end ); + // Average multi-byte ratio + $ratio = $mb_length / strlen( $str ); + // Base64 has a 4:3 ratio + $avgLength = floor( $length * $ratio * .75 ); + + for ( $i = 0; $i < $mb_length; $i += $offset ) { + $lookBack = 0; + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr( $str, $i, $offset, $this->CharSet ); + $chunk = base64_encode( $chunk ); + $lookBack ++; + } while ( strlen( $chunk ) > $length ); + $encoded .= $chunk . $linebreak; + } + + // Chomp the last linefeed + $encoded = substr( $encoded, 0, - strlen( $linebreak ) ); + + return $encoded; + } + + /** + * Encode a string using Q encoding. + * @link http://tools.ietf.org/html/rfc2047 + * + * @param string $str the text to encode + * @param string $position Where the text is going to be used, see the RFC for what that means + * + * @access public + * @return string + */ + public function encodeQ( $str, $position = 'text' ) { + // There should not be any EOL in the string + $pattern = ''; + $encoded = str_replace( array( "\r", "\n" ), '', $str ); + switch ( strtolower( $position ) ) { + case 'phrase': + // RFC 2047 section 5.3 + $pattern = '^A-Za-z0-9!*+\/ -'; + break; + /** @noinspection PhpMissingBreakStatementInspection */ + case 'comment': + // RFC 2047 section 5.2 + $pattern = '\(\)"'; + // intentional fall-through + // for this reason we build the $pattern without including delimiters and [] + case 'text': + default: + // RFC 2047 section 5.1 + // Replace every high ascii, control, =, ? and _ characters + $pattern = '\000-\011\013\014\016-\037\075\077\137\177-\377' . $pattern; + break; + } + $matches = array(); + if ( preg_match_all( "/[{$pattern}]/", $encoded, $matches ) ) { + // If the string contains an '=', make sure it's the first thing we replace + // so as to avoid double-encoding + $eqkey = array_search( '=', $matches[0] ); + if ( false !== $eqkey ) { + unset( $matches[0][ $eqkey ] ); + array_unshift( $matches[0], '=' ); + } + foreach ( array_unique( $matches[0] ) as $char ) { + $encoded = str_replace( $char, '=' . sprintf( '%02X', ord( $char ) ), $encoded ); + } + } + + // Replace every spaces to _ (more readable than =20) + return str_replace( ' ', '_', $encoded ); + } + + /** + * Word-wrap message. + * For use with mailers that do not automatically perform wrapping + * and for quoted-printable encoded messages. + * Original written by philippe. + * + * @param string $message The message to wrap + * @param integer $length The line length to wrap to + * @param boolean $qp_mode Whether to run in Quoted-Printable mode + * + * @access public + * @return string + */ + public function wrapText( $message, $length, $qp_mode = false ) { + if ( $qp_mode ) { + $soft_break = sprintf( ' =%s', $this->LE ); + } else { + $soft_break = $this->LE; + } + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = ( strtolower( $this->CharSet ) == 'utf-8' ); + $lelen = strlen( $this->LE ); + $crlflen = strlen( self::CRLF ); + + $message = $this->fixEOL( $message ); + //Remove a trailing line break + if ( substr( $message, - $lelen ) == $this->LE ) { + $message = substr( $message, 0, - $lelen ); + } + + //Split message into lines + $lines = explode( $this->LE, $message ); + //Message will be rebuilt in here + $message = ''; + foreach ( $lines as $line ) { + $words = explode( ' ', $line ); + $buf = ''; + $firstword = true; + foreach ( $words as $word ) { + if ( $qp_mode and ( strlen( $word ) > $length ) ) { + $space_left = $length - strlen( $buf ) - $crlflen; + if ( ! $firstword ) { + if ( $space_left > 20 ) { + $len = $space_left; + if ( $is_utf8 ) { + $len = $this->utf8CharBoundary( $word, $len ); + } elseif ( substr( $word, $len - 1, 1 ) == '=' ) { + $len --; + } elseif ( substr( $word, $len - 2, 1 ) == '=' ) { + $len -= 2; + } + $part = substr( $word, 0, $len ); + $word = substr( $word, $len ); + $buf .= ' ' . $part; + $message .= $buf . sprintf( '=%s', self::CRLF ); + } else { + $message .= $buf . $soft_break; + } + $buf = ''; + } + while ( strlen( $word ) > 0 ) { + if ( $length <= 0 ) { + break; + } + $len = $length; + if ( $is_utf8 ) { + $len = $this->utf8CharBoundary( $word, $len ); + } elseif ( substr( $word, $len - 1, 1 ) == '=' ) { + $len --; + } elseif ( substr( $word, $len - 2, 1 ) == '=' ) { + $len -= 2; + } + $part = substr( $word, 0, $len ); + $word = substr( $word, $len ); + + if ( strlen( $word ) > 0 ) { + $message .= $part . sprintf( '=%s', self::CRLF ); + } else { + $buf = $part; + } + } + } else { + $buf_o = $buf; + if ( ! $firstword ) { + $buf .= ' '; + } + $buf .= $word; + + if ( strlen( $buf ) > $length and $buf_o != '' ) { + $message .= $buf_o . $soft_break; + $buf = $word; + } + } + $firstword = false; + } + $message .= $buf . self::CRLF; + } + + return $message; + } + + /** + * Ensure consistent line endings in a string. + * Changes every end of line from CRLF, CR or LF to $this->LE. + * @access public + * + * @param string $str String to fixEOL + * + * @return string + */ + public function fixEOL( $str ) { + // Normalise to \n + $nstr = str_replace( array( "\r\n", "\r" ), "\n", $str ); + // Now convert LE as needed + if ( $this->LE !== "\n" ) { + $nstr = str_replace( "\n", $this->LE, $nstr ); + } + + return $nstr; + } + + /** + * Find the last character boundary prior to $maxLength in a utf-8 + * quoted-printable encoded string. + * Original written by Colin Brown. + * @access public + * + * @param string $encodedText utf-8 QP text + * @param integer $maxLength Find the last character boundary prior to this length + * + * @return integer + */ + public function utf8CharBoundary( $encodedText, $maxLength ) { + $foundSplitPos = false; + $lookBack = 3; + while ( ! $foundSplitPos ) { + $lastChunk = substr( $encodedText, $maxLength - $lookBack, $lookBack ); + $encodedCharPos = strpos( $lastChunk, '=' ); + if ( false !== $encodedCharPos ) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr( $encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2 ); + $dec = hexdec( $hex ); + if ( $dec < 128 ) { + // Single byte character. + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + if ( $encodedCharPos > 0 ) { + $maxLength = $maxLength - ( $lookBack - $encodedCharPos ); + } + $foundSplitPos = true; + } elseif ( $dec >= 192 ) { + // First byte of a multi byte character + // Reduce maxLength to split at start of character + $maxLength = $maxLength - ( $lookBack - $encodedCharPos ); + $foundSplitPos = true; + } elseif ( $dec < 192 ) { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + + return $maxLength; + } + + /** + * Get the array of strings for the current language. + * @return array + */ + public function getTranslations() { + return $this->language; + } + + /** + * Returns the whole MIME message. + * Includes complete headers and body. + * Only valid post preSend(). + * @see PHPMailer::preSend() + * @access public + * @return string + */ + public function getSentMIMEMessage() { + return rtrim( $this->MIMEHeader . $this->mailHeader, "\n\r" ) . self::CRLF . self::CRLF . $this->MIMEBody; + } + + /** + * Add an attachment from a path on the filesystem. + * Returns false if the file could not be found or read. + * + * @param string $path Path to the attachment. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @param string $disposition Disposition to use + * + * @throws phpmailerException + * @return boolean + */ + public function addAttachment( $path, $name = '', $encoding = 'base64', $type = '', $disposition = 'attachment' ) { + try { + if ( ! @is_file( $path ) ) { + throw new phpmailerException( $this->lang( 'file_access' ) . $path, self::STOP_CONTINUE ); + } + + // If a MIME type is not specified, try to work it out from the file name + if ( $type == '' ) { + $type = self::filenameToType( $path ); + } + + $filename = basename( $path ); + if ( $name == '' ) { + $name = $filename; + } + + $this->attachment[] = array( + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => 0 + ); + + } catch ( phpmailerException $exc ) { + $this->setError( $exc->getMessage() ); + $this->edebug( $exc->getMessage() ); + if ( $this->exceptions ) { + throw $exc; + } + + return false; + } + + return true; + } + + /** + * Map a file name to a MIME type. + * Defaults to 'application/octet-stream', i.e.. arbitrary binary data. + * + * @param string $filename A file name or full path, does not need to exist as a file + * + * @return string + * @static + */ + public static function filenameToType( $filename ) { + // In case the path is a URL, strip any query string before getting extension + $qpos = strpos( $filename, '?' ); + if ( false !== $qpos ) { + $filename = substr( $filename, 0, $qpos ); + } + $pathinfo = self::mb_pathinfo( $filename ); + + return self::_mime_types( $pathinfo['extension'] ); + } + + /** + * Multi-byte-safe pathinfo replacement. + * Drop-in replacement for pathinfo(), but multibyte-safe, cross-platform-safe, old-version-safe. + * Works similarly to the one in PHP >= 5.2.0 + * @link http://www.php.net/manual/en/function.pathinfo.php#107461 + * + * @param string $path A filename or path, does not need to exist as a file + * @param integer|string $options Either a PATHINFO_* constant, + * or a string name to return only the specified piece, allows 'filename' to work on PHP < 5.2 + * + * @return string|array + * @static + */ + public static function mb_pathinfo( $path, $options = null ) { + $ret = array( 'dirname' => '', 'basename' => '', 'extension' => '', 'filename' => '' ); + $pathinfo = array(); + if ( preg_match( '%^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$%im', $path, $pathinfo ) ) { + if ( array_key_exists( 1, $pathinfo ) ) { + $ret['dirname'] = $pathinfo[1]; + } + if ( array_key_exists( 2, $pathinfo ) ) { + $ret['basename'] = $pathinfo[2]; + } + if ( array_key_exists( 5, $pathinfo ) ) { + $ret['extension'] = $pathinfo[5]; + } + if ( array_key_exists( 3, $pathinfo ) ) { + $ret['filename'] = $pathinfo[3]; + } + } + switch ( $options ) { + case PATHINFO_DIRNAME: + case 'dirname': + return $ret['dirname']; + case PATHINFO_BASENAME: + case 'basename': + return $ret['basename']; + case PATHINFO_EXTENSION: + case 'extension': + return $ret['extension']; + case PATHINFO_FILENAME: + case 'filename': + return $ret['filename']; + default: + return $ret; + } + } + + /** + * Get the MIME type for a file extension. + * + * @param string $ext File extension + * + * @access public + * @return string MIME type of file. + * @static + */ + public static function _mime_types( $ext = '' ) { + $mimes = array( + 'xl' => 'application/excel', + 'js' => 'application/javascript', + 'hqx' => 'application/mac-binhex40', + 'cpt' => 'application/mac-compactpro', + 'bin' => 'application/macbinary', + 'doc' => 'application/msword', + 'word' => 'application/msword', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'class' => 'application/octet-stream', + 'dll' => 'application/octet-stream', + 'dms' => 'application/octet-stream', + 'exe' => 'application/octet-stream', + 'lha' => 'application/octet-stream', + 'lzh' => 'application/octet-stream', + 'psd' => 'application/octet-stream', + 'sea' => 'application/octet-stream', + 'so' => 'application/octet-stream', + 'oda' => 'application/oda', + 'pdf' => 'application/pdf', + 'ai' => 'application/postscript', + 'eps' => 'application/postscript', + 'ps' => 'application/postscript', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'mif' => 'application/vnd.mif', + 'xls' => 'application/vnd.ms-excel', + 'ppt' => 'application/vnd.ms-powerpoint', + 'wbxml' => 'application/vnd.wap.wbxml', + 'wmlc' => 'application/vnd.wap.wmlc', + 'dcr' => 'application/x-director', + 'dir' => 'application/x-director', + 'dxr' => 'application/x-director', + 'dvi' => 'application/x-dvi', + 'gtar' => 'application/x-gtar', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'php' => 'application/x-httpd-php', + 'phtml' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'swf' => 'application/x-shockwave-flash', + 'sit' => 'application/x-stuffit', + 'tar' => 'application/x-tar', + 'tgz' => 'application/x-tar', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'zip' => 'application/zip', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mp2' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mpga' => 'audio/mpeg', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'ram' => 'audio/x-pn-realaudio', + 'rm' => 'audio/x-pn-realaudio', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'ra' => 'audio/x-realaudio', + 'wav' => 'audio/x-wav', + 'bmp' => 'image/bmp', + 'gif' => 'image/gif', + 'jpeg' => 'image/jpeg', + 'jpe' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'png' => 'image/png', + 'tiff' => 'image/tiff', + 'tif' => 'image/tiff', + 'eml' => 'message/rfc822', + 'css' => 'text/css', + 'html' => 'text/html', + 'htm' => 'text/html', + 'shtml' => 'text/html', + 'log' => 'text/plain', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'rtx' => 'text/richtext', + 'rtf' => 'text/rtf', + 'vcf' => 'text/vcard', + 'vcard' => 'text/vcard', + 'xml' => 'text/xml', + 'xsl' => 'text/xml', + 'mpeg' => 'video/mpeg', + 'mpe' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mov' => 'video/quicktime', + 'qt' => 'video/quicktime', + 'rv' => 'video/vnd.rn-realvideo', + 'avi' => 'video/x-msvideo', + 'movie' => 'video/x-sgi-movie' + ); + if ( array_key_exists( strtolower( $ext ), $mimes ) ) { + return $mimes[ strtolower( $ext ) ]; + } + + return 'application/octet-stream'; + } + + /** + * Return the array of attachments. + * @return array + */ + public function getAttachments() { + return $this->attachment; + } + + /** + * Backward compatibility wrapper for an old QP encoding function that was removed. + * @see PHPMailer::encodeQP() + * @access public + * + * @param string $string + * @param integer $line_max + * @param boolean $space_conv + * + * @return string + * @deprecated Use encodeQP instead. + */ + public function encodeQPphp( + $string, + $line_max = 76, + /** @noinspection PhpUnusedParameterInspection */ + $space_conv = false + ) { + return $this->encodeQP( $string, $line_max ); + } + + /** + * Add a string or binary attachment (non-filesystem). + * This method can be used to attach ascii or binary data, + * such as a BLOB record from a database. + * + * @param string $string String attachment data. + * @param string $filename Name of the attachment. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File extension (MIME) type. + * @param string $disposition Disposition to use + * + * @return void + */ + public function addStringAttachment( + $string, + $filename, + $encoding = 'base64', + $type = '', + $disposition = 'attachment' + ) { + // If a MIME type is not specified, try to work it out from the file name + if ( $type == '' ) { + $type = self::filenameToType( $filename ); + } + // Append to $attachment array + $this->attachment[] = array( + 0 => $string, + 1 => $filename, + 2 => basename( $filename ), + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => 0 + ); + } + + /** + * Clear all To recipients. + * @return void + */ + public function clearAddresses() { + foreach ( $this->to as $to ) { + unset( $this->all_recipients[ strtolower( $to[0] ) ] ); + } + $this->to = array(); + $this->clearQueuedAddresses( 'to' ); + } + + /** + * Clear queued addresses of given kind. + * @access protected + * + * @param string $kind 'to', 'cc', or 'bcc' + * + * @return void + */ + public function clearQueuedAddresses( $kind ) { + $RecipientsQueue = $this->RecipientsQueue; + foreach ( $RecipientsQueue as $address => $params ) { + if ( $params[0] == $kind ) { + unset( $this->RecipientsQueue[ $address ] ); + } + } + } + + /** + * Clear all CC recipients. + * @return void + */ + public function clearCCs() { + foreach ( $this->cc as $cc ) { + unset( $this->all_recipients[ strtolower( $cc[0] ) ] ); + } + $this->cc = array(); + $this->clearQueuedAddresses( 'cc' ); + } + + /** + * Clear all BCC recipients. + * @return void + */ + public function clearBCCs() { + foreach ( $this->bcc as $bcc ) { + unset( $this->all_recipients[ strtolower( $bcc[0] ) ] ); + } + $this->bcc = array(); + $this->clearQueuedAddresses( 'bcc' ); + } + + /** + * Clear all ReplyTo recipients. + * @return void + */ + public function clearReplyTos() { + $this->ReplyTo = array(); + $this->ReplyToQueue = array(); + } + + /** + * Clear all recipient types. + * @return void + */ + public function clearAllRecipients() { + $this->to = array(); + $this->cc = array(); + $this->bcc = array(); + $this->all_recipients = array(); + $this->RecipientsQueue = array(); + } + + /** + * Clear all filesystem, string, and binary attachments. + * @return void + */ + public function clearAttachments() { + $this->attachment = array(); + } + + /** + * Clear all custom headers. + * @return void + */ + public function clearCustomHeaders() { + $this->CustomHeader = array(); + } + + /** + * Add a custom header. + * $name value can be overloaded to contain + * both header name and value (name:value) + * @access public + * + * @param string $name Custom header name + * @param string $value Header value + * + * @return void + */ + public function addCustomHeader( $name, $value = null ) { + if ( $value === null ) { + // Value passed in as name:value + $this->CustomHeader[] = explode( ':', $name, 2 ); + } else { + $this->CustomHeader[] = array( $name, $value ); + } + } + + /** + * Returns all custom headers. + * @return array + */ + public function getCustomHeaders() { + return $this->CustomHeader; + } + + /** + * Create a message from an HTML string. + * Automatically makes modifications for inline images and backgrounds + * and creates a plain-text version by converting the HTML. + * Overwrites any existing values in $this->Body and $this->AltBody + * @access public + * + * @param string $message HTML message string + * @param string $basedir baseline directory for path + * @param boolean|callable $advanced Whether to use the internal HTML to text converter + * or your own custom converter @see PHPMailer::html2text() + * + * @return string $message + */ + public function msgHTML( $message, $basedir = '', $advanced = false ) { + preg_match_all( '/(src|background)=["\'](.*)["\']/Ui', $message, $images ); + if ( array_key_exists( 2, $images ) ) { + foreach ( $images[2] as $imgindex => $url ) { + // Convert data URIs into embedded images + if ( preg_match( '#^data:(image[^;,]*)(;base64)?,#', $url, $match ) ) { + $data = substr( $url, strpos( $url, ',' ) ); + if ( $match[2] ) { + $data = base64_decode( $data ); + } else { + $data = rawurldecode( $data ); + } + $cid = md5( $url ) . '@phpmailer.0'; // RFC2392 S 2 + if ( $this->addStringEmbeddedImage( $data, $cid, 'embed' . $imgindex, 'base64', $match[1] ) ) { + $message = str_replace( + $images[0][ $imgindex ], + $images[1][ $imgindex ] . '="cid:' . $cid . '"', + $message + ); + } + } elseif ( substr( $url, 0, 4 ) !== 'cid:' && ! preg_match( '#^[a-z][a-z0-9+.-]*://#i', $url ) ) { + // Do not change urls for absolute images (thanks to corvuscorax) + // Do not change urls that are already inline images + $filename = basename( $url ); + $directory = dirname( $url ); + if ( $directory == '.' ) { + $directory = ''; + } + $cid = md5( $url ) . '@phpmailer.0'; // RFC2392 S 2 + if ( strlen( $basedir ) > 1 && substr( $basedir, - 1 ) != '/' ) { + $basedir .= '/'; + } + if ( strlen( $directory ) > 1 && substr( $directory, - 1 ) != '/' ) { + $directory .= '/'; + } + if ( $this->addEmbeddedImage( + $basedir . $directory . $filename, + $cid, + $filename, + 'base64', + self::_mime_types( (string) self::mb_pathinfo( $filename, PATHINFO_EXTENSION ) ) + ) + ) { + $message = preg_replace( + '/' . $images[1][ $imgindex ] . '=["\']' . preg_quote( $url, '/' ) . '["\']/Ui', + $images[1][ $imgindex ] . '="cid:' . $cid . '"', + $message + ); + } + } + } + } + $this->isHTML( true ); + // Convert all message body line breaks to CRLF, makes quoted-printable encoding work much better + $this->Body = $this->normalizeBreaks( $message ); + $this->AltBody = $this->normalizeBreaks( $this->html2text( $message, $advanced ) ); + if ( ! $this->alternativeExists() ) { + $this->AltBody = 'To view this email message, open it in a program that understands HTML!' . + self::CRLF . self::CRLF; + } + + return $this->Body; + } + + /** + * Add an embedded stringified attachment. + * This can include images, sounds, and just about any other document type. + * Be sure to set the $type to an image type for images: + * JPEG images use 'image/jpeg', GIF uses 'image/gif', PNG uses 'image/png'. + * + * @param string $string The attachment binary data. + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML. + * @param string $name + * @param string $encoding File encoding (see $Encoding). + * @param string $type MIME type. + * @param string $disposition Disposition to use + * + * @return boolean True on successfully adding an attachment + */ + public function addStringEmbeddedImage( + $string, + $cid, + $name = '', + $encoding = 'base64', + $type = '', + $disposition = 'inline' + ) { + // If a MIME type is not specified, try to work it out from the name + if ( $type == '' and ! empty( $name ) ) { + $type = self::filenameToType( $name ); + } + + // Append to $attachment array + $this->attachment[] = array( + 0 => $string, + 1 => $name, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => true, // isStringAttachment + 6 => $disposition, + 7 => $cid + ); + + return true; + } + + /** + * Add an embedded (inline) attachment from a file. + * This can include images, sounds, and just about any other document type. + * These differ from 'regular' attachments in that they are intended to be + * displayed inline with the message, not just attached for download. + * This is used in HTML messages that embed the images + * the HTML refers to using the $cid value. + * + * @param string $path Path to the attachment. + * @param string $cid Content ID of the attachment; Use this to reference + * the content when using an embedded image in HTML. + * @param string $name Overrides the attachment name. + * @param string $encoding File encoding (see $Encoding). + * @param string $type File MIME type. + * @param string $disposition Disposition to use + * + * @return boolean True on successfully adding an attachment + */ + public function addEmbeddedImage( $path, $cid, $name = '', $encoding = 'base64', $type = '', $disposition = 'inline' ) { + if ( ! @is_file( $path ) ) { + $this->setError( $this->lang( 'file_access' ) . $path ); + + return false; + } + + // If a MIME type is not specified, try to work it out from the file name + if ( $type == '' ) { + $type = self::filenameToType( $path ); + } + + $filename = basename( $path ); + if ( $name == '' ) { + $name = $filename; + } + + // Append to $attachment array + $this->attachment[] = array( + 0 => $path, + 1 => $filename, + 2 => $name, + 3 => $encoding, + 4 => $type, + 5 => false, // isStringAttachment + 6 => $disposition, + 7 => $cid + ); + + return true; + } + + /** + * Sets message type to HTML or plain. + * + * @param boolean $isHtml True for HTML mode. + * + * @return void + */ + public function isHTML( $isHtml = true ) { + if ( $isHtml ) { + $this->ContentType = 'text/html'; + } else { + $this->ContentType = 'text/plain'; + } + } + + /** + * Normalize line breaks in a string. + * Converts UNIX LF, Mac CR and Windows CRLF line breaks into a single line break format. + * Defaults to CRLF (for message bodies) and preserves consecutive breaks. + * + * @param string $text + * @param string $breaktype What kind of line break to use, defaults to CRLF + * + * @return string + * @access public + * @static + */ + public static function normalizeBreaks( $text, $breaktype = "\r\n" ) { + return preg_replace( '/(\r\n|\r|\n)/ms', $breaktype, $text ); + } + + /** + * Convert an HTML string into plain text. + * This is used by msgHTML(). + * Note - older versions of this function used a bundled advanced converter + * which was been removed for license reasons in #232 + * Example usage: + * + * // Use default conversion + * $plain = $mail->html2text($html); + * // Use your own custom converter + * $plain = $mail->html2text($html, function($html) { + * $converter = new MyHtml2text($html); + * return $converter->get_text(); + * }); + * + * + * @param string $html The HTML text to convert + * @param boolean|callable $advanced Any boolean value to use the internal converter, + * or provide your own callable for custom conversion. + * + * @return string + */ + public function html2text( $html, $advanced = false ) { + if ( is_callable( $advanced ) ) { + return call_user_func( $advanced, $html ); + } + + return html_entity_decode( + trim( strip_tags( preg_replace( '/<(head|title|style|script)[^>]*>.*?<\/\\1>/si', '', $html ) ) ), + ENT_QUOTES, + $this->CharSet + ); + } + + /** + * Set or reset instance properties. + * You should avoid this function - it's more verbose, less efficient, more error-prone and + * harder to debug than setting properties directly. + * Usage Example: + * `$mail->set('SMTPSecure', 'tls');` + * is the same as: + * `$mail->SMTPSecure = 'tls';` + * @access public + * + * @param string $name The property name to set + * @param mixed $value The value to set the property to + * + * @return boolean + */ + public function set( $name, $value = '' ) { + if ( property_exists( $this, $name ) ) { + $this->$name = $value; + + return true; + } else { + $this->setError( $this->lang( 'variable_set' ) . $name ); + + return false; + } + } + + /** + * Set the public and private key files and password for S/MIME signing. + * @access public + * + * @param string $cert_filename + * @param string $key_filename + * @param string $key_pass Password for private key + * @param string $extracerts_filename Optional path to chain certificate + */ + public function sign( $cert_filename, $key_filename, $key_pass, $extracerts_filename = '' ) { + $this->sign_cert_file = $cert_filename; + $this->sign_key_file = $key_filename; + $this->sign_key_pass = $key_pass; + $this->sign_extracerts_file = $extracerts_filename; + } + + /** + * Allows for public read access to 'to' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getToAddresses() { + return $this->to; + } + + /** + * Allows for public read access to 'cc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getCcAddresses() { + return $this->cc; + } + + /** + * Allows for public read access to 'bcc' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getBccAddresses() { + return $this->bcc; + } + + /** + * Allows for public read access to 'ReplyTo' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getReplyToAddresses() { + return $this->ReplyTo; + } + + /** + * Allows for public read access to 'all_recipients' property. + * @note: Before the send() call, queued addresses (i.e. with IDN) are not yet included. + * @access public + * @return array + */ + public function getAllRecipientAddresses() { + return $this->all_recipients; + } + + /** + * Add an address to one of the recipient arrays or to the ReplyTo array. + * Addresses that have been added already return false, but do not throw exceptions. + * + * @param string $kind One of 'to', 'cc', 'bcc', or 'ReplyTo' + * @param string $address The email address to send, resp. to reply to + * @param string $name + * + * @throws phpmailerException + * @return boolean true on success, false if address already used or invalid in some way + * @access protected + */ + protected function addAnAddress( $kind, $address, $name = '' ) { + if ( ! in_array( $kind, array( 'to', 'cc', 'bcc', 'Reply-To' ) ) ) { + $error_message = $this->lang( 'Invalid recipient kind: ' ) . $kind; + $this->setError( $error_message ); + $this->edebug( $error_message ); + if ( $this->exceptions ) { + throw new phpmailerException( $error_message ); + } + + return false; + } + if ( ! $this->validateAddress( $address ) ) { + $error_message = $this->lang( 'invalid_address' ) . " (addAnAddress $kind): $address"; + $this->setError( $error_message ); + $this->edebug( $error_message ); + if ( $this->exceptions ) { + throw new phpmailerException( $error_message ); + } + + return false; + } + if ( $kind != 'Reply-To' ) { + if ( ! array_key_exists( strtolower( $address ), $this->all_recipients ) ) { + array_push( $this->$kind, array( $address, $name ) ); + $this->all_recipients[ strtolower( $address ) ] = true; + + return true; + } + } else { + if ( ! array_key_exists( strtolower( $address ), $this->ReplyTo ) ) { + $this->ReplyTo[ strtolower( $address ) ] = array( $address, $name ); + + return true; + } + } + + return false; + } + + /** + * Check that a string looks like an email address. + * + * @param string $address The email address to check + * @param string $patternselect A selector for the validation pattern to use : + * * `auto` Pick best pattern automatically; + * * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14; + * * `pcre` Use old PCRE implementation; + * * `php` Use PHP built-in FILTER_VALIDATE_EMAIL; + * * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements. + * * `noregex` Don't use a regex: super fast, really dumb. + * + * @return boolean + * @static + * @access public + */ + public static function validateAddress( $address, $patternselect = 'auto' ) { + //Reject line breaks in addresses; it's valid RFC5322, but not RFC5321 + if ( strpos( $address, "\n" ) !== false or strpos( $address, "\r" ) !== false ) { + return false; + } + if ( ! $patternselect or $patternselect == 'auto' ) { + //Check this constant first so it works when extension_loaded() is disabled by safe mode + //Constant was added in PHP 5.2.4 + if ( defined( 'PCRE_VERSION' ) ) { + //This pattern can get stuck in a recursive loop in PCRE <= 8.0.2 + if ( version_compare( PCRE_VERSION, '8.0.3' ) >= 0 ) { + $patternselect = 'pcre8'; + } else { + $patternselect = 'pcre'; + } + } elseif ( function_exists( 'extension_loaded' ) and extension_loaded( 'pcre' ) ) { + //Fall back to older PCRE + $patternselect = 'pcre'; + } else { + //Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension + if ( version_compare( PHP_VERSION, '5.2.0' ) >= 0 ) { + $patternselect = 'php'; + } else { + $patternselect = 'noregex'; + } + } + } + switch ( $patternselect ) { + case 'pcre8': + /** + * Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains. + * @link http://squiloople.com/2009/12/20/email-address-validation/ + * @copyright 2009-2010 Michael Rushton + * Feel free to use and redistribute this code. But please keep this copyright notice. + */ + return (boolean) preg_match( + '/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' . + '((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' . + '(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' . + '([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' . + '(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' . + '(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' . + '|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' . + '|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD', + $address + ); + case 'pcre': + //An older regex that doesn't need a recent PCRE + return (boolean) preg_match( + '/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' . + '[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' . + '(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' . + '@(?>(?![a-z0-9-]{64,})(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>\.(?![a-z0-9-]{64,})' . + '(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)){0,126}|\[(?:(?>IPv6:(?>(?>[a-f0-9]{1,4})(?>:' . + '[a-f0-9]{1,4}){7}|(?!(?:.*[a-f0-9][:\]]){8,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?' . + '::(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?))|(?>(?>IPv6:(?>[a-f0-9]{1,4}(?>:' . + '[a-f0-9]{1,4}){5}:|(?!(?:.*[a-f0-9]:){6,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4})?' . + '::(?>(?:[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4}):)?))?(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}' . + '|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD', + $address + ); + case 'html5': + /** + * This is the pattern used in the HTML5 spec for validation of 'email' type form input elements. + * @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email) + */ + return (boolean) preg_match( + '/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' . + '[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD', + $address + ); + case 'noregex': + //No PCRE! Do something _very_ approximate! + //Check the address is 3 chars or longer and contains an @ that's not the first or last char + return ( strlen( $address ) >= 3 + and strpos( $address, '@' ) >= 1 + and strpos( $address, '@' ) != strlen( $address ) - 1 ); + case 'php': + default: + return (boolean) filter_var( $address, FILTER_VALIDATE_EMAIL ); + } + } +} + +/** + * PHPMailer exception handler + * @package PHPMailer + */ +class phpmailerException extends Exception { + /** + * Prettify error message output + * @return string + */ + public function errorMessage() { + $errorMsg = '' . $this->getMessage() . "
\n"; + + return $errorMsg; + } +} diff --git a/src/lib/mail/class.smtp.php b/src/lib/mail/class.smtp.php new file mode 100644 index 0000000..af41bf2 --- /dev/null +++ b/src/lib/mail/class.smtp.php @@ -0,0 +1,1214 @@ + + * @author Jim Jagielski (jimjag) + * @author Andy Prevost (codeworxtech) + * @author Brent R. Matzelle (original founder) + * @copyright 2014 Marcus Bointon + * @copyright 2010 - 2012 Jim Jagielski + * @copyright 2004 - 2009 Andy Prevost + * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License + * @note This program is distributed in the hope that it will be useful - WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + */ + +/** + * PHPMailer RFC821 SMTP email transport class. + * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. + * @package PHPMailer + * @author Chris Ryan + * @author Marcus Bointon + */ +class SMTP { + /** + * The PHPMailer SMTP version number. + * @var string + */ + const VERSION = '5.2.14'; + + /** + * SMTP line break constant. + * @var string + */ + const CRLF = "\r\n"; + + /** + * The SMTP port to use if one is not specified. + * @var integer + */ + const DEFAULT_SMTP_PORT = 25; + + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @var integer + */ + const MAX_LINE_LENGTH = 998; + + /** + * Debug level for no output + */ + const DEBUG_OFF = 0; + + /** + * Debug level to show client -> server messages + */ + const DEBUG_CLIENT = 1; + + /** + * Debug level to show client -> server and server -> client messages + */ + const DEBUG_SERVER = 2; + + /** + * Debug level to show connection status, client -> server and server -> client messages + */ + const DEBUG_CONNECTION = 3; + + /** + * Debug level to show all messages + */ + const DEBUG_LOWLEVEL = 4; + + /** + * The PHPMailer SMTP Version number. + * @var string + * @deprecated Use the `VERSION` constant instead + * @see SMTP::VERSION + */ + public $Version = '5.2.14'; + + /** + * SMTP server port number. + * @var integer + * @deprecated This is only ever used as a default value, so use the `DEFAULT_SMTP_PORT` constant instead + * @see SMTP::DEFAULT_SMTP_PORT + */ + public $SMTP_PORT = 25; + + /** + * SMTP reply line ending. + * @var string + * @deprecated Use the `CRLF` constant instead + * @see SMTP::CRLF + */ + public $CRLF = "\r\n"; + + /** + * Debug output level. + * Options: + * * self::DEBUG_OFF (`0`) No debug output, default + * * self::DEBUG_CLIENT (`1`) Client commands + * * self::DEBUG_SERVER (`2`) Client commands and server responses + * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status + * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages + * @var integer + */ + public $do_debug = self::DEBUG_OFF; + + /** + * How to handle debug output. + * Options: + * * `echo` Output plain-text as-is, appropriate for CLI + * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output + * * `error_log` Output to error log as configured in php.ini + * + * Alternatively, you can provide a callable expecting two params: a message string and the debug level: + * + * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; + * + * @var string|callable + */ + public $Debugoutput = 'echo'; + + /** + * Whether to use VERP. + * @link http://en.wikipedia.org/wiki/Variable_envelope_return_path + * @link http://www.postfix.org/VERP_README.html Info on VERP + * @var boolean + */ + public $do_verp = false; + + /** + * The timeout value for connection, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. + * @link http://tools.ietf.org/html/rfc2821#section-4.5.3.2 + * @var integer + */ + public $Timeout = 300; + + /** + * How long to wait for commands to complete, in seconds. + * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2 + * @var integer + */ + public $Timelimit = 300; + + /** + * The socket for the server connection. + * @var resource + */ + protected $smtp_conn; + + /** + * Error information, if any, for the last SMTP command. + * @var array + */ + protected $error = array( + 'error' => '', + 'detail' => '', + 'smtp_code' => '', + 'smtp_code_ex' => '' + ); + + /** + * The reply the server sent to us for HELO. + * If null, no HELO string has yet been received. + * @var string|null + */ + protected $helo_rply = null; + + /** + * The set of SMTP extensions sent in reply to EHLO command. + * Indexes of the array are extension names. + * Value at index 'HELO' or 'EHLO' (according to command that was sent) + * represents the server name. In case of HELO it is the only element of the array. + * Other values can be boolean TRUE or an array containing extension options. + * If null, no HELO/EHLO string has yet been received. + * @var array|null + */ + protected $server_caps = null; + + /** + * The most recent reply received from the server. + * @var string + */ + protected $last_reply = ''; + + /** + * Connect to an SMTP server. + * + * @param string $host SMTP server IP or host name + * @param integer $port The port number to connect to + * @param integer $timeout How long to wait for the connection to open + * @param array $options An array of options for stream_context_create() + * + * @access public + * @return boolean + */ + public function connect( $host, $port = null, $timeout = 30, $options = array() ) { + static $streamok; + //This is enabled by default since 5.0.0 but some providers disable it + //Check this once and cache the result + if ( is_null( $streamok ) ) { + $streamok = function_exists( 'stream_socket_client' ); + } + // Clear errors to avoid confusion + $this->setError( '' ); + // Make sure we are __not__ connected + if ( $this->connected() ) { + // Already connected, generate error + $this->setError( 'Already connected to a server' ); + + return false; + } + if ( empty( $port ) ) { + $port = self::DEFAULT_SMTP_PORT; + } + // Connect to the SMTP server + $this->edebug( + "Connection: opening to $host:$port, timeout=$timeout, options=" . var_export( $options, true ), + self::DEBUG_CONNECTION + ); + $errno = 0; + $errstr = ''; + if ( $streamok ) { + $socket_context = stream_context_create( $options ); + //Suppress errors; connection failures are handled at a higher level + $this->smtp_conn = @stream_socket_client( + $host . ":" . $port, + $errno, + $errstr, + $timeout, + STREAM_CLIENT_CONNECT, + $socket_context + ); + } else { + //Fall back to fsockopen which should work in more places, but is missing some features + $this->edebug( + "Connection: stream_socket_client not available, falling back to fsockopen", + self::DEBUG_CONNECTION + ); + $this->smtp_conn = fsockopen( + $host, + $port, + $errno, + $errstr, + $timeout + ); + } + // Verify we connected properly + if ( ! is_resource( $this->smtp_conn ) ) { + $this->setError( + 'Failed to connect to server', + $errno, + $errstr + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] + . ": $errstr ($errno)", + self::DEBUG_CLIENT + ); + + return false; + } + $this->edebug( 'Connection: opened', self::DEBUG_CONNECTION ); + // SMTP server can take longer to respond, give longer timeout for first read + // Windows does not have support for this timeout function + if ( substr( PHP_OS, 0, 3 ) != 'WIN' ) { + $max = ini_get( 'max_execution_time' ); + // Don't bother if unlimited + if ( $max != 0 && $timeout > $max ) { + @set_time_limit( $timeout ); + } + stream_set_timeout( $this->smtp_conn, $timeout, 0 ); + } + // Get any announcement + $announce = $this->get_lines(); + $this->edebug( 'SERVER -> CLIENT: ' . $announce, self::DEBUG_SERVER ); + + return true; + } + + /** + * Check connection state. + * @access public + * @return boolean True if connected. + */ + public function connected() { + if ( is_resource( $this->smtp_conn ) ) { + $sock_status = stream_get_meta_data( $this->smtp_conn ); + if ( $sock_status['eof'] ) { + // The socket is valid but we are not connected + $this->edebug( + 'SMTP NOTICE: EOF caught while checking if connected', + self::DEBUG_CLIENT + ); + $this->close(); + + return false; + } + + return true; // everything looks good + } + + return false; + } + + /** + * Output debugging info via a user-selected method. + * @see SMTP::$Debugoutput + * @see SMTP::$do_debug + * + * @param string $str Debug string to output + * @param integer $level The debug level of this message; see DEBUG_* constants + * + * @return void + */ + protected function edebug( $str, $level = 0 ) { + if ( $level > $this->do_debug ) { + return; + } + //Avoid clash with built-in function names + if ( ! in_array( $this->Debugoutput, array( + 'error_log', + 'html', + 'echo' + ) ) and is_callable( $this->Debugoutput ) ) { + call_user_func( $this->Debugoutput, $str, $this->do_debug ); + + return; + } + switch ( $this->Debugoutput ) { + case 'error_log': + //Don't output, just log + error_log( $str ); + break; + case 'html': + //Cleans up output a bit for a better looking, HTML-safe output + echo htmlentities( + preg_replace( '/[\r\n]+/', '', $str ), + ENT_QUOTES, + 'UTF-8' + ) + . "
\n"; + break; + case 'echo': + default: + //Normalize line breaks + $str = preg_replace( '/(\r\n|\r|\n)/ms', "\n", $str ); + echo gmdate( 'Y-m-d H:i:s' ) . "\t" . str_replace( + "\n", + "\n \t ", + trim( $str ) + ) . "\n"; + } + } + + /** + * Close the socket and clean up the state of the class. + * Don't use this function without first trying to use QUIT. + * @see quit() + * @access public + * @return void + */ + public function close() { + $this->setError( '' ); + $this->server_caps = null; + $this->helo_rply = null; + if ( is_resource( $this->smtp_conn ) ) { + // close the connection and cleanup + fclose( $this->smtp_conn ); + $this->smtp_conn = null; //Makes for cleaner serialization + $this->edebug( 'Connection: closed', self::DEBUG_CONNECTION ); + } + } + + /** + * Read the SMTP server's response. + * Either before eof or socket timeout occurs on the operation. + * With SMTP we can tell if we have more lines to read if the + * 4th character is '-' symbol. If it is a space then we don't + * need to read anything else. + * @access protected + * @return string + */ + protected function get_lines() { + // If the connection is bad, give up straight away + if ( ! is_resource( $this->smtp_conn ) ) { + return ''; + } + $data = ''; + $endtime = 0; + stream_set_timeout( $this->smtp_conn, $this->Timeout ); + if ( $this->Timelimit > 0 ) { + $endtime = time() + $this->Timelimit; + } + while ( is_resource( $this->smtp_conn ) && ! feof( $this->smtp_conn ) ) { + $str = @fgets( $this->smtp_conn, 515 ); + $this->edebug( "SMTP -> get_lines(): \$data is \"$data\"", self::DEBUG_LOWLEVEL ); + $this->edebug( "SMTP -> get_lines(): \$str is \"$str\"", self::DEBUG_LOWLEVEL ); + $data .= $str; + // If 4th character is a space, we are done reading, break the loop, micro-optimisation over strlen + if ( ( isset( $str[3] ) and $str[3] == ' ' ) ) { + break; + } + // Timed-out? Log and break + $info = stream_get_meta_data( $this->smtp_conn ); + if ( $info['timed_out'] ) { + $this->edebug( + 'SMTP -> get_lines(): timed-out (' . $this->Timeout . ' sec)', + self::DEBUG_LOWLEVEL + ); + break; + } + // Now check if reads took too long + if ( $endtime and time() > $endtime ) { + $this->edebug( + 'SMTP -> get_lines(): timelimit reached (' . + $this->Timelimit . ' sec)', + self::DEBUG_LOWLEVEL + ); + break; + } + } + + return $data; + } + + /** + * Initiate a TLS (encrypted) session. + * @access public + * @return boolean + */ + public function startTLS() { + if ( ! $this->sendCommand( 'STARTTLS', 'STARTTLS', 220 ) ) { + return false; + } + // Begin encrypted connection + if ( ! stream_socket_enable_crypto( + $this->smtp_conn, + true, + STREAM_CRYPTO_METHOD_TLS_CLIENT + ) ) { + return false; + } + + return true; + } + + /** + * Send a command to an SMTP server and check its return code. + * + * @param string $command The command name - not sent to the server + * @param string $commandstring The actual command to send + * @param integer|array $expect One or more expected integer success codes + * + * @access protected + * @return boolean True on success. + */ + protected function sendCommand( $command, $commandstring, $expect ) { + if ( ! $this->connected() ) { + $this->setError( "Called $command without being connected" ); + + return false; + } + //Reject line breaks in all commands + if ( strpos( $commandstring, "\n" ) !== false or strpos( $commandstring, "\r" ) !== false ) { + $this->setError( "Command '$command' contained line breaks" ); + + return false; + } + $this->client_send( $commandstring . self::CRLF ); + + $this->last_reply = $this->get_lines(); + // Fetch SMTP code and possible error code explanation + $matches = array(); + if ( preg_match( "/^([0-9]{3})[ -](?:([0-9]\\.[0-9]\\.[0-9]) )?/", $this->last_reply, $matches ) ) { + $code = $matches[1]; + $code_ex = ( count( $matches ) > 2 ? $matches[2] : null ); + // Cut off error code from each response line + $detail = preg_replace( + "/{$code}[ -]" . ( $code_ex ? str_replace( '.', '\\.', $code_ex ) . ' ' : '' ) . "/m", + '', + $this->last_reply + ); + } else { + // Fall back to simple parsing if regex fails + $code = substr( $this->last_reply, 0, 3 ); + $code_ex = null; + $detail = substr( $this->last_reply, 4 ); + } + + $this->edebug( 'SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER ); + + if ( ! in_array( $code, (array) $expect ) ) { + $this->setError( + "$command command failed", + $detail, + $code, + $code_ex + ); + $this->edebug( + 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, + self::DEBUG_CLIENT + ); + + return false; + } + + $this->setError( '' ); + + return true; + } + + /** + * Send raw data to the server. + * + * @param string $data The data to send + * + * @access public + * @return integer|boolean The number of bytes sent to the server or false on error + */ + public function client_send( $data ) { + $this->edebug( "CLIENT -> SERVER: $data", self::DEBUG_CLIENT ); + + return fwrite( $this->smtp_conn, $data ); + } + + /** + * Perform SMTP authentication. + * Must be run after hello(). + * @see hello() + * + * @param string $username The user name + * @param string $password The password + * @param string $authtype The auth type (PLAIN, LOGIN, NTLM, CRAM-MD5, XOAUTH2) + * @param string $realm The auth realm for NTLM + * @param string $workstation The auth workstation for NTLM + * @param null|OAuth $OAuth An optional OAuth instance (@see PHPMailerOAuth) + * + * @return bool True if successfully authenticated.* @access public + */ + public function authenticate( + $username, + $password, + $authtype = null, + $realm = '', + $workstation = '', + $OAuth = null + ) { + if ( ! $this->server_caps ) { + $this->setError( 'Authentication is not allowed before HELO/EHLO' ); + + return false; + } + + if ( array_key_exists( 'EHLO', $this->server_caps ) ) { + // SMTP extensions are available. Let's try to find a proper authentication method + + if ( ! array_key_exists( 'AUTH', $this->server_caps ) ) { + $this->setError( 'Authentication is not allowed at this stage' ); + // 'at this stage' means that auth may be allowed after the stage changes + // e.g. after STARTTLS + return false; + } + + self::edebug( 'Auth method requested: ' . ( $authtype ? $authtype : 'UNKNOWN' ), self::DEBUG_LOWLEVEL ); + self::edebug( + 'Auth methods available on the server: ' . implode( ',', $this->server_caps['AUTH'] ), + self::DEBUG_LOWLEVEL + ); + + if ( empty( $authtype ) ) { + foreach ( array( 'LOGIN', 'CRAM-MD5', 'NTLM', 'PLAIN', 'XOAUTH2' ) as $method ) { + if ( in_array( $method, $this->server_caps['AUTH'] ) ) { + $authtype = $method; + break; + } + } + if ( empty( $authtype ) ) { + $this->setError( 'No supported authentication methods found' ); + + return false; + } + self::edebug( 'Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL ); + } + + if ( ! in_array( $authtype, $this->server_caps['AUTH'] ) ) { + $this->setError( "The requested authentication method \"$authtype\" is not supported by the server" ); + + return false; + } + } elseif ( empty( $authtype ) ) { + $authtype = 'LOGIN'; + } + switch ( $authtype ) { + case 'PLAIN': + // Start authentication + if ( ! $this->sendCommand( 'AUTH', 'AUTH PLAIN', 334 ) ) { + return false; + } + // Send encoded username and password + if ( ! $this->sendCommand( + 'User & Password', + base64_encode( "\0" . $username . "\0" . $password ), + 235 + ) + ) { + return false; + } + break; + case 'LOGIN': + // Start authentication + if ( ! $this->sendCommand( 'AUTH', 'AUTH LOGIN', 334 ) ) { + return false; + } + if ( ! $this->sendCommand( "Username", base64_encode( $username ), 334 ) ) { + return false; + } + if ( ! $this->sendCommand( "Password", base64_encode( $password ), 235 ) ) { + return false; + } + break; + case 'XOAUTH2': + //If the OAuth Instance is not set. Can be a case when PHPMailer is used + //instead of PHPMailerOAuth + if ( is_null( $OAuth ) ) { + return false; + } + $oauth = $OAuth->getOauth64(); + + // Start authentication + if ( ! $this->sendCommand( 'AUTH', 'AUTH XOAUTH2 ' . $oauth, 235 ) ) { + return false; + } + break; + case 'NTLM': + /* + * ntlm_sasl_client.php + * Bundled with Permission + * + * How to telnet in windows: + * http://technet.microsoft.com/en-us/library/aa995718%28EXCHG.65%29.aspx + * PROTOCOL Docs http://curl.haxx.se/rfc/ntlm.html#ntlmSmtpAuthentication + */ + require_once 'extras/ntlm_sasl_client.php'; + $temp = new stdClass; + $ntlm_client = new ntlm_sasl_client_class; + //Check that functions are available + if ( ! $ntlm_client->Initialize( $temp ) ) { + $this->setError( $temp->error ); + $this->edebug( + 'You need to enable some modules in your php.ini file: ' + . $this->error['error'], + self::DEBUG_CLIENT + ); + + return false; + } + //msg1 + $msg1 = $ntlm_client->TypeMsg1( $realm, $workstation ); //msg1 + + if ( ! $this->sendCommand( + 'AUTH NTLM', + 'AUTH NTLM ' . base64_encode( $msg1 ), + 334 + ) + ) { + return false; + } + //Though 0 based, there is a white space after the 3 digit number + //msg2 + $challenge = substr( $this->last_reply, 3 ); + $challenge = base64_decode( $challenge ); + $ntlm_res = $ntlm_client->NTLMResponse( + substr( $challenge, 24, 8 ), + $password + ); + //msg3 + $msg3 = $ntlm_client->TypeMsg3( + $ntlm_res, + $username, + $realm, + $workstation + ); + + // send encoded username + return $this->sendCommand( 'Username', base64_encode( $msg3 ), 235 ); + case 'CRAM-MD5': + // Start authentication + if ( ! $this->sendCommand( 'AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334 ) ) { + return false; + } + // Get the challenge + $challenge = base64_decode( substr( $this->last_reply, 4 ) ); + + // Build the response + $response = $username . ' ' . $this->hmac( $challenge, $password ); + + // send encoded credentials + return $this->sendCommand( 'Username', base64_encode( $response ), 235 ); + default: + $this->setError( "Authentication method \"$authtype\" is not supported" ); + + return false; + } + + return true; + } + + /** + * Calculate an MD5 HMAC hash. + * Works like hash_hmac('md5', $data, $key) + * in case that function is not available + * + * @param string $data The data to hash + * @param string $key The key to hash with + * + * @access protected + * @return string + */ + protected function hmac( $data, $key ) { + if ( function_exists( 'hash_hmac' ) ) { + return hash_hmac( 'md5', $data, $key ); + } + + // The following borrowed from + // http://php.net/manual/en/function.mhash.php#27225 + + // RFC 2104 HMAC implementation for php. + // Creates an md5 HMAC. + // Eliminates the need to install mhash to compute a HMAC + // by Lance Rushing + + $bytelen = 64; // byte length for md5 + if ( strlen( $key ) > $bytelen ) { + $key = pack( 'H*', md5( $key ) ); + } + $key = str_pad( $key, $bytelen, chr( 0x00 ) ); + $ipad = str_pad( '', $bytelen, chr( 0x36 ) ); + $opad = str_pad( '', $bytelen, chr( 0x5c ) ); + $k_ipad = $key ^ $ipad; + $k_opad = $key ^ $opad; + + return md5( $k_opad . pack( 'H*', md5( $k_ipad . $data ) ) ); + } + + /** + * Send an SMTP DATA command. + * Issues a data command and sends the msg_data to the server, + * finializing the mail transaction. $msg_data is the message + * that is to be send with the headers. Each header needs to be + * on a single line followed by a with the message headers + * and the message body being separated by and additional . + * Implements rfc 821: DATA + * + * @param string $msg_data Message data to send + * + * @access public + * @return boolean + */ + public function data( $msg_data ) { + //This will use the standard timelimit + if ( ! $this->sendCommand( 'DATA', 'DATA', 354 ) ) { + return false; + } + + /* The server is ready to accept data! + * According to rfc821 we should not send more than 1000 characters on a single line (including the CRLF) + * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into + * smaller lines to fit within the limit. + * We will also look for lines that start with a '.' and prepend an additional '.'. + * NOTE: this does not count towards line-length limit. + */ + + // Normalize line breaks before exploding + $lines = explode( "\n", str_replace( array( "\r\n", "\r" ), "\n", $msg_data ) ); + + /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field + * of the first line (':' separated) does not contain a space then it _should_ be a header and we will + * process all lines before a blank line as headers. + */ + + $field = substr( $lines[0], 0, strpos( $lines[0], ':' ) ); + $in_headers = false; + if ( ! empty( $field ) && strpos( $field, ' ' ) === false ) { + $in_headers = true; + } + + foreach ( $lines as $line ) { + $lines_out = array(); + if ( $in_headers and $line == '' ) { + $in_headers = false; + } + //Break this line up into several smaller lines if it's too long + //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), + while ( isset( $line[ self::MAX_LINE_LENGTH ] ) ) { + //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on + //so as to avoid breaking in the middle of a word + $pos = strrpos( substr( $line, 0, self::MAX_LINE_LENGTH ), ' ' ); + //Deliberately matches both false and 0 + if ( ! $pos ) { + //No nice break found, add a hard break + $pos = self::MAX_LINE_LENGTH - 1; + $lines_out[] = substr( $line, 0, $pos ); + $line = substr( $line, $pos ); + } else { + //Break at the found point + $lines_out[] = substr( $line, 0, $pos ); + //Move along by the amount we dealt with + $line = substr( $line, $pos + 1 ); + } + //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 + if ( $in_headers ) { + $line = "\t" . $line; + } + } + $lines_out[] = $line; + + //Send the lines to the server + foreach ( $lines_out as $line_out ) { + //RFC2821 section 4.5.2 + if ( ! empty( $line_out ) and $line_out[0] == '.' ) { + $line_out = '.' . $line_out; + } + $this->client_send( $line_out . self::CRLF ); + } + } + + //Message data has been sent, complete the command + //Increase timelimit for end of DATA command + $savetimelimit = $this->Timelimit; + $this->Timelimit = $this->Timelimit * 2; + $result = $this->sendCommand( 'DATA END', '.', 250 ); + //Restore timelimit + $this->Timelimit = $savetimelimit; + + return $result; + } + + /** + * Send an SMTP HELO or EHLO command. + * Used to identify the sending server to the receiving server. + * This makes sure that client and server are in a known state. + * Implements RFC 821: HELO + * and RFC 2821 EHLO. + * + * @param string $host The host name or IP to connect to + * + * @access public + * @return boolean + */ + public function hello( $host = '' ) { + //Try extended hello first (RFC 2821) + return (boolean) ( $this->sendHello( 'EHLO', $host ) or $this->sendHello( 'HELO', $host ) ); + } + + /** + * Send an SMTP HELO or EHLO command. + * Low-level implementation used by hello() + * @see hello() + * + * @param string $hello The HELO string + * @param string $host The hostname to say we are + * + * @access protected + * @return boolean + */ + protected function sendHello( $hello, $host ) { + $noerror = $this->sendCommand( $hello, $hello . ' ' . $host, 250 ); + $this->helo_rply = $this->last_reply; + if ( $noerror ) { + $this->parseHelloFields( $hello ); + } else { + $this->server_caps = null; + } + + return $noerror; + } + + /** + * Parse a reply to HELO/EHLO command to discover server extensions. + * In case of HELO, the only parameter that can be discovered is a server name. + * @access protected + * + * @param string $type - 'HELO' or 'EHLO' + */ + protected function parseHelloFields( $type ) { + $this->server_caps = array(); + $lines = explode( "\n", $this->last_reply ); + + foreach ( $lines as $n => $s ) { + //First 4 chars contain response code followed by - or space + $s = trim( substr( $s, 4 ) ); + if ( empty( $s ) ) { + continue; + } + $fields = explode( ' ', $s ); + if ( ! empty( $fields ) ) { + if ( ! $n ) { + $name = $type; + $fields = $fields[0]; + } else { + $name = array_shift( $fields ); + switch ( $name ) { + case 'SIZE': + $fields = ( $fields ? $fields[0] : 0 ); + break; + case 'AUTH': + if ( ! is_array( $fields ) ) { + $fields = array(); + } + break; + default: + $fields = true; + } + } + $this->server_caps[ $name ] = $fields; + } + } + } + + /** + * Send an SMTP MAIL command. + * Starts a mail transaction from the email address specified in + * $from. Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. + * Implements rfc 821: MAIL FROM: + * + * @param string $from Source address of this message + * + * @access public + * @return boolean + */ + public function mail( $from ) { + $useVerp = ( $this->do_verp ? ' XVERP' : '' ); + + return $this->sendCommand( + 'MAIL FROM', + 'MAIL FROM:<' . $from . '>' . $useVerp, + 250 + ); + } + + /** + * Send an SMTP QUIT command. + * Closes the socket if there is no error or the $close_on_error argument is true. + * Implements from rfc 821: QUIT + * + * @param boolean $close_on_error Should the connection close if an error occurs? + * + * @access public + * @return boolean + */ + public function quit( $close_on_error = true ) { + $noerror = $this->sendCommand( 'QUIT', 'QUIT', 221 ); + $err = $this->error; //Save any error + if ( $noerror or $close_on_error ) { + $this->close(); + $this->error = $err; //Restore any error from the quit command + } + + return $noerror; + } + + /** + * Send an SMTP RCPT command. + * Sets the TO argument to $toaddr. + * Returns true if the recipient was accepted false if it was rejected. + * Implements from rfc 821: RCPT TO: + * + * @param string $address The address the message is being sent to + * + * @access public + * @return boolean + */ + public function recipient( $address ) { + return $this->sendCommand( + 'RCPT TO', + 'RCPT TO:<' . $address . '>', + array( 250, 251 ) + ); + } + + /** + * Send an SMTP RSET command. + * Abort any transaction that is currently in progress. + * Implements rfc 821: RSET + * @access public + * @return boolean True on success. + */ + public function reset() { + return $this->sendCommand( 'RSET', 'RSET', 250 ); + } + + /** + * Send an SMTP SAML command. + * Starts a mail transaction from the email address specified in $from. + * Returns true if successful or false otherwise. If True + * the mail transaction is started and then one or more recipient + * commands may be called followed by a data command. This command + * will send the message to the users terminal if they are logged + * in and send them an email. + * Implements rfc 821: SAML FROM: + * + * @param string $from The address the message is from + * + * @access public + * @return boolean + */ + public function sendAndMail( $from ) { + return $this->sendCommand( 'SAML', "SAML FROM:$from", 250 ); + } + + /** + * Send an SMTP VRFY command. + * + * @param string $name The name to verify + * + * @access public + * @return boolean + */ + public function verify( $name ) { + return $this->sendCommand( 'VRFY', "VRFY $name", array( 250, 251 ) ); + } + + /** + * Send an SMTP NOOP command. + * Used to keep keep-alives alive, doesn't actually do anything + * @access public + * @return boolean + */ + public function noop() { + return $this->sendCommand( 'NOOP', 'NOOP', 250 ); + } + + /** + * Send an SMTP TURN command. + * This is an optional command for SMTP that this class does not support. + * This method is here to make the RFC821 Definition complete for this class + * and _may_ be implemented in future + * Implements from rfc 821: TURN + * @access public + * @return boolean + */ + public function turn() { + $this->setError( 'The SMTP TURN command is not implemented' ); + $this->edebug( 'SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT ); + + return false; + } + + /** + * Get the latest error. + * @access public + * @return array + */ + public function getError() { + return $this->error; + } + + /** + * Set error messages and codes. + * + * @param string $message The error message + * @param string $detail Further detail on the error + * @param string $smtp_code An associated SMTP error code + * @param string $smtp_code_ex Extended SMTP code + */ + protected function setError( $message, $detail = '', $smtp_code = '', $smtp_code_ex = '' ) { + $this->error = array( + 'error' => $message, + 'detail' => $detail, + 'smtp_code' => $smtp_code, + 'smtp_code_ex' => $smtp_code_ex + ); + } + + /** + * Get SMTP extensions available on the server + * @access public + * @return array|null + */ + public function getServerExtList() { + return $this->server_caps; + } + + /** + * A multipurpose method + * The method works in three ways, dependent on argument value and current state + * 1. HELO/EHLO was not sent - returns null and set up $this->error + * 2. HELO was sent + * $name = 'HELO': returns server name + * $name = 'EHLO': returns boolean false + * $name = any string: returns null and set up $this->error + * 3. EHLO was sent + * $name = 'HELO'|'EHLO': returns server name + * $name = any string: if extension $name exists, returns boolean True + * or its options. Otherwise returns boolean False + * In other words, one can use this method to detect 3 conditions: + * - null returned: handshake was not or we don't know about ext (refer to $this->error) + * - false returned: the requested feature exactly not exists + * - positive value returned: the requested feature exists + * + * @param string $name Name of SMTP extension or 'HELO'|'EHLO' + * + * @return mixed + */ + public function getServerExt( $name ) { + if ( ! $this->server_caps ) { + $this->setError( 'No HELO/EHLO was sent' ); + + return null; + } + + // the tight logic knot ;) + if ( ! array_key_exists( $name, $this->server_caps ) ) { + if ( $name == 'HELO' ) { + return $this->server_caps['EHLO']; + } + if ( $name == 'EHLO' || array_key_exists( 'EHLO', $this->server_caps ) ) { + return false; + } + $this->setError( 'HELO handshake was used. Client knows nothing about server extensions' ); + + return null; + } + + return $this->server_caps[ $name ]; + } + + /** + * Get the last reply from the server. + * @access public + * @return string + */ + public function getLastReply() { + return $this->last_reply; + } + + /** + * Enable or disable VERP address generation. + * + * @param boolean $enabled + */ + public function setVerp( $enabled = false ) { + $this->do_verp = $enabled; + } + + /** + * Get VERP address generation mode. + * @return boolean + */ + public function getVerp() { + return $this->do_verp; + } + + /** + * Get debug output method. + * @return string + */ + public function getDebugOutput() { + return $this->Debugoutput; + } + + /** + * Set debug output method. + * + * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it. + */ + public function setDebugOutput( $method = 'echo' ) { + $this->Debugoutput = $method; + } + + /** + * Set debug output level. + * + * @param integer $level + */ + public function setDebugLevel( $level = 0 ) { + $this->do_debug = $level; + } + + /** + * Get debug output level. + * @return integer + */ + public function getDebugLevel() { + return $this->do_debug; + } + + /** + * Get SMTP timeout. + * @return integer + */ + public function getTimeout() { + return $this->Timeout; + } + + /** + * Set SMTP timeout. + * + * @param integer $timeout + */ + public function setTimeout( $timeout = 0 ) { + $this->Timeout = $timeout; + } +} diff --git a/src/lib/mail/mail_default.html b/src/lib/mail/mail_default.html new file mode 100644 index 0000000..1949812 --- /dev/null +++ b/src/lib/mail/mail_default.html @@ -0,0 +1,15 @@ + + + + + + +
+ +
+
+ +
+ + diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..12c8ed7 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,55 @@ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +body { + line-height: 1; +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +* { + padding: 0; + margin: 0; + box-sizing: border-box; +} diff --git a/static/js/javascript.js b/static/js/javascript.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/jquery-3.2.1.min.js b/static/js/jquery-3.2.1.min.js new file mode 100644 index 0000000..644d35e --- /dev/null +++ b/static/js/jquery-3.2.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("