├── README.md └── php └── support ├── sdk_remotedapi.php ├── request.php ├── crc32_stream.php ├── webroute.php ├── deflate_stream.php ├── sdk_barebones_cms_lite.php ├── utf_utils.php ├── web_browser.php └── sdk_barebones_cms_api.php /README.md: -------------------------------------------------------------------------------- 1 | Barebones CMS SDKs 2 | ================== 3 | 4 | This is the Barebones CMS SDKs development repository for contributors. Changes made here propagate to the [Barebones CMS release distribution](https://github.com/cubiclesoft/barebones-cms). 5 | 6 | Deploying the software in this repository directly to production environments is not supported. 7 | 8 | Barebones CMS is MIT or LGPL, your choice. 9 | 10 | Contributing 11 | ------------ 12 | 13 | Open an issue on the issue tracker OR fork the project, make your changes, and start a pull request. 14 | 15 | For changes that only affect a couple of lines of code, using the issue tracker is easier. 16 | -------------------------------------------------------------------------------- /php/support/sdk_remotedapi.php: -------------------------------------------------------------------------------- 1 | false, "error" => WebRoute::WRTranslate("Invalid Remoted API URL scheme."), "errorcode" => "invalid_scheme"); 45 | 46 | $result["url"] = $url; 47 | 48 | return $result; 49 | } 50 | 51 | if ($url2["loginusername"] === "") return array("success" => false, "error" => WebRoute::WRTranslate("Remoted API URL is missing client key."), "errorcode" => "missing_client_key"); 52 | 53 | $options["headers"]["X-Remoted-APIKey"] = $url2["loginusername"]; 54 | 55 | $url2["scheme"] = ($url2["scheme"] === "rwr" ? "wr" : "wrs"); 56 | unset($url2["loginusername"]); 57 | unset($url2["login"]); 58 | 59 | $url = HTTP::CondenseURL($url2); 60 | 61 | $result = $wr->Connect($url, false, $timeout, $options, $web); 62 | if (!$result["success"]) return $result; 63 | 64 | $options["fp"] = $result["fp"]; 65 | } 66 | 67 | return $result; 68 | } 69 | } 70 | ?> -------------------------------------------------------------------------------- /php/support/request.php: -------------------------------------------------------------------------------- 1 | $val) 12 | { 13 | if (is_string($val)) $_REQUEST[$key] = trim($val); 14 | else if (is_array($val)) 15 | { 16 | $_REQUEST[$key] = array(); 17 | foreach ($val as $key2 => $val2) $_REQUEST[$key][$key2] = (is_string($val2) ? trim($val2) : $val2); 18 | } 19 | else $_REQUEST[$key] = $val; 20 | } 21 | } 22 | 23 | // Cleans up all PHP input issues so that $_REQUEST may be used as expected. 24 | public static function Normalize() 25 | { 26 | self::ProcessSingleInput($_COOKIE); 27 | self::ProcessSingleInput($_GET); 28 | self::ProcessSingleInput($_POST); 29 | } 30 | 31 | public static function IsSSL() 32 | { 33 | return ((isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] == "1")) || (isset($_SERVER["SERVER_PORT"]) && $_SERVER["SERVER_PORT"] == "443") || (isset($_SERVER["REQUEST_URI"]) && str_replace("\\", "/", strtolower(substr($_SERVER["REQUEST_URI"], 0, 8))) == "https://")); 34 | } 35 | 36 | // Returns 'http[s]://www.something.com[:port]' based on the current page request. 37 | public static function GetHost($protocol = "") 38 | { 39 | $protocol = strtolower($protocol); 40 | $ssl = ($protocol == "https" || ($protocol == "" && self::IsSSL())); 41 | if ($protocol == "") $type = "def"; 42 | else if ($ssl) $type = "https"; 43 | else $type = "http"; 44 | 45 | if (!isset(self::$hostcache)) self::$hostcache = array(); 46 | if (isset(self::$hostcache[$type])) return self::$hostcache[$type]; 47 | 48 | $url = "http" . ($ssl ? "s" : "") . "://"; 49 | 50 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 51 | $pos = strpos($str, "?"); 52 | if ($pos !== false) $str = substr($str, 0, $pos); 53 | $str2 = strtolower($str); 54 | if (substr($str2, 0, 7) == "http://") 55 | { 56 | $pos = strpos($str, "/", 7); 57 | if ($pos === false) $str = ""; 58 | else $str = substr($str, 7, $pos); 59 | } 60 | else if (substr($str2, 0, 8) == "https://") 61 | { 62 | $pos = strpos($str, "/", 8); 63 | if ($pos === false) $str = ""; 64 | else $str = substr($str, 8, $pos); 65 | } 66 | else $str = ""; 67 | 68 | if ($str != "") $host = $str; 69 | else if (isset($_SERVER["HTTP_HOST"])) $host = $_SERVER["HTTP_HOST"]; 70 | else $host = $_SERVER["SERVER_NAME"] . ":" . (int)$_SERVER["SERVER_PORT"]; 71 | 72 | $pos = strpos($host, ":"); 73 | if ($pos === false) $port = 0; 74 | else 75 | { 76 | $port = (int)substr($host, $pos + 1); 77 | $host = substr($host, 0, $pos); 78 | } 79 | if ($port < 1 || $port > 65535) $port = ($ssl ? 443 : 80); 80 | $url .= preg_replace('/[^a-z0-9.\-]/', "", strtolower($host)); 81 | if ($protocol == "" && ((!$ssl && $port != 80) || ($ssl && $port != 443))) $url .= ":" . $port; 82 | else if ($protocol == "http" && !$ssl && $port != 80) $url .= ":" . $port; 83 | else if ($protocol == "https" && $ssl && $port != 443) $url .= ":" . $port; 84 | 85 | self::$hostcache[$type] = $url; 86 | 87 | return $url; 88 | } 89 | 90 | public static function GetURLBase() 91 | { 92 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 93 | $pos = strpos($str, "?"); 94 | if ($pos !== false) $str = substr($str, 0, $pos); 95 | if (strncasecmp($str, "http://", 7) == 0 || strncasecmp($str, "https://", 8) == 0) 96 | { 97 | $pos = strpos($str, "/", 8); 98 | if ($pos === false) $str = "/"; 99 | else $str = substr($str, $pos); 100 | } 101 | 102 | return $str; 103 | } 104 | 105 | public static function GetFullURLBase($protocol = "") 106 | { 107 | return self::GetHost($protocol) . self::GetURLBase(); 108 | } 109 | 110 | public static function PrependHost($url, $protocol = "") 111 | { 112 | // Handle protocol-only. 113 | if (strncmp($url, "//", 2) == 0) 114 | { 115 | $host = self::GetHost($protocol); 116 | $pos = strpos($host, ":"); 117 | if ($pos === false) return $url; 118 | 119 | return substr($host, 0, $pos + 1) . $url; 120 | } 121 | 122 | if (strpos($url, ":") !== false) return $url; 123 | 124 | // Handle relative paths. 125 | if ($url === "" || $url[0] !== "/") return rtrim(self::GetFullURLBase($protocol), "/") . "/" . $url; 126 | 127 | // Handle absolute paths. 128 | $host = self::GetHost($protocol); 129 | 130 | return $host . $url; 131 | } 132 | } 133 | ?> -------------------------------------------------------------------------------- /php/support/crc32_stream.php: -------------------------------------------------------------------------------- 1 | 0x04C11DB7, "start" => 0xFFFFFFFF, "xor" => 0xFFFFFFFF, "refdata" => 1, "refcrc" => 1); 14 | 15 | public function __construct() 16 | { 17 | $this->open = false; 18 | } 19 | 20 | public function Init($options = false) 21 | { 22 | if ($options === false && function_exists("hash_init")) $this->hash = hash_init("crc32b"); 23 | else 24 | { 25 | if ($options === false) $options = self::$default; 26 | 27 | $this->hash = false; 28 | $this->crctable = array(); 29 | $poly = $this->LIM32($options["poly"]); 30 | for ($x = 0; $x < 256; $x++) 31 | { 32 | $c = $this->SHL32($x, 24); 33 | for ($y = 0; $y < 8; $y++) $c = $this->SHL32($c, 1) ^ ($c & 0x80000000 ? $poly : 0); 34 | $this->crctable[$x] = $c; 35 | } 36 | 37 | $this->datareflect = $options["refdata"]; 38 | $this->crcreflect = $options["refcrc"]; 39 | $this->firstcrc = $options["start"]; 40 | $this->currcrc = $options["start"]; 41 | $this->finalxor = $options["xor"]; 42 | } 43 | 44 | $this->open = true; 45 | } 46 | 47 | public function AddData($data) 48 | { 49 | if (!$this->open) return false; 50 | 51 | if ($this->hash !== false) hash_update($this->hash, $data); 52 | else 53 | { 54 | $y = strlen($data); 55 | 56 | for ($x = 0; $x < $y; $x++) 57 | { 58 | if ($this->datareflect) $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ self::$revlookup[ord($data[$x])]]; 59 | else $this->currcrc = $this->SHL32($this->currcrc, 8) ^ $this->crctable[$this->SHR32($this->currcrc, 24) ^ ord($data[$x])]; 60 | } 61 | } 62 | 63 | return true; 64 | } 65 | 66 | public function Finalize() 67 | { 68 | if (!$this->open) return false; 69 | 70 | if ($this->hash !== false) 71 | { 72 | $result = hexdec(hash_final($this->hash)); 73 | 74 | $this->hash = hash_init("crc32b"); 75 | } 76 | else 77 | { 78 | if ($this->crcreflect) 79 | { 80 | $tempcrc = $this->currcrc; 81 | $this->currcrc = self::$revlookup[$this->SHR32($tempcrc, 24)] | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 16) & 0xFF], 8) | $this->SHL32(self::$revlookup[$this->SHR32($tempcrc, 8) & 0xFF], 16) | $this->SHL32(self::$revlookup[$this->LIM32($tempcrc & 0xFF)], 24); 82 | } 83 | $result = $this->currcrc ^ $this->finalxor; 84 | 85 | $this->currcrc = $this->firstcrc; 86 | } 87 | 88 | return $result; 89 | } 90 | 91 | // These functions are a hacky, but effective way of enforcing unsigned 32-bit integers onto a generic signed int. 92 | // Allow bitwise operations to work across platforms. Minimum integer size must be 32-bit. 93 | private function SHR32($num, $bits) 94 | { 95 | $num = (int)$num; 96 | if ($bits < 0) $bits = 0; 97 | 98 | if ($num < 0 && $bits) 99 | { 100 | $num = ($num >> 1) & 0x7FFFFFFF; 101 | $bits--; 102 | } 103 | 104 | return $this->LIM32($num >> $bits); 105 | } 106 | 107 | private function SHL32($num, $bits) 108 | { 109 | if ($bits < 0) $bits = 0; 110 | 111 | return $this->LIM32((int)$num << $bits); 112 | } 113 | 114 | private function LIM32($num) 115 | { 116 | return (int)((int)$num & 0xFFFFFFFF); 117 | } 118 | } 119 | ?> -------------------------------------------------------------------------------- /php/support/webroute.php: -------------------------------------------------------------------------------- 1 | csprng = false; 17 | } 18 | 19 | public static function ProcessState($state) 20 | { 21 | while ($state->state !== "done") 22 | { 23 | switch ($state->state) 24 | { 25 | case "initialize": 26 | { 27 | $result = $state->web->Process($state->url, $state->options); 28 | if (!$result["success"]) return $result; 29 | 30 | if (isset($state->options["async"]) && $state->options["async"]) 31 | { 32 | $state->async = true; 33 | $state->webstate = $result["state"]; 34 | 35 | $state->state = "process_async"; 36 | } 37 | else 38 | { 39 | $state->result = $result; 40 | 41 | $state->state = "post_retrieval"; 42 | } 43 | 44 | break; 45 | } 46 | case "process_async": 47 | { 48 | // Run a cycle of the WebBrowser state processor. 49 | $result = $state->web->ProcessState($state->webstate); 50 | if (!$result["success"]) return $result; 51 | 52 | $state->webstate = false; 53 | $state->result = $result; 54 | 55 | $state->state = "post_retrieval"; 56 | 57 | break; 58 | } 59 | case "post_retrieval": 60 | { 61 | if ($state->result["response"]["code"] != 101) return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() failed to connect to the WebRoute. Server returned: %s %s", $result["response"]["code"], $result["response"]["meaning"]), "errorcode" => "incorrect_server_response"); 62 | if (!isset($state->result["headers"]["Sec-Webroute-Accept"])) return array("success" => false, "error" => self::WRTranslate("Server failed to include a 'Sec-WebRoute-Accept' header in its response to the request."), "errorcode" => "missing_server_webroute_accept_header"); 63 | 64 | // Verify the Sec-WebRoute-Accept response. 65 | if ($state->result["headers"]["Sec-Webroute-Accept"][0] !== base64_encode(sha1($state->options["headers"]["WebRoute-ID"] . self::ID_GUID, true))) return array("success" => false, "error" => self::WRTranslate("The server's 'Sec-WebRoute-Accept' header is invalid."), "errorcode" => "invalid_server_webroute_accept_header"); 66 | 67 | $state->state = "done"; 68 | 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return $state->result; 75 | } 76 | 77 | public function Connect($url, $id = false, $timeout = false, $options = array(), $web = false) 78 | { 79 | // Generate client ID. 80 | if ($id === false) 81 | { 82 | if (!class_exists("CSPRNG", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/random.php"; 83 | 84 | if ($this->csprng === false) $this->csprng = new CSPRNG(); 85 | 86 | $id = $this->csprng->GenerateString(64); 87 | } 88 | 89 | if (!class_exists("WebBrowser", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/web_browser.php"; 90 | 91 | // Use WebBrowser to initiate the connection. 92 | if ($web === false) $web = new WebBrowser(); 93 | 94 | // Transform URL. 95 | $url2 = HTTP::ExtractURL($url); 96 | if ($url2["scheme"] != "wr" && $url2["scheme"] != "wrs") return array("success" => false, "error" => self::WRTranslate("WebRoute::Connect() only supports the 'wr' and 'wrs' protocols."), "errorcode" => "protocol_check"); 97 | $url2["scheme"] = str_replace("wr", "http", $url2["scheme"]); 98 | $url2 = HTTP::CondenseURL($url2); 99 | 100 | // Generate correct request headers. 101 | if (!isset($options["headers"])) $options["headers"] = array(); 102 | $options["headers"]["Connection"] = "keep-alive, Upgrade"; 103 | $options["headers"]["Pragma"] = "no-cache"; 104 | $options["headers"]["WebRoute-Version"] = "1"; 105 | $options["headers"]["WebRoute-ID"] = $id; 106 | if ($timeout !== false && is_int($timeout)) $options["headers"]["WebRoute-Timeout"] = (string)(int)$timeout; 107 | $options["headers"]["Upgrade"] = "webroute"; 108 | 109 | // Initialize the process state object. 110 | $state = new stdClass(); 111 | $state->async = false; 112 | $state->state = "initialize"; 113 | $state->web = $web; 114 | $state->url = $url2; 115 | $state->options = $options; 116 | $state->webstate = false; 117 | $state->result = false; 118 | 119 | // Run at least one state cycle to finish initializing the state object. 120 | $result = $this->ProcessState($state); 121 | 122 | // Return the state for async calls. Caller must call ProcessState(). 123 | if ($state->async) return array("success" => true, "id" => $id, "state" => $state); 124 | 125 | $result["id"] = $id; 126 | 127 | return $result; 128 | } 129 | 130 | // Implements the correct MultiAsyncHelper responses for WebRoute instances. 131 | public function ConnectAsync__Handler($mode, &$data, $key, $info) 132 | { 133 | switch ($mode) 134 | { 135 | case "init": 136 | { 137 | if ($info->init) $data = $info->keep; 138 | else 139 | { 140 | $info->result = $this->Connect($info->url, $info->id, $info->timeout, $info->options, $info->web); 141 | if (!$info->result["success"]) 142 | { 143 | $info->keep = false; 144 | 145 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 146 | } 147 | else 148 | { 149 | $info->id = $info->result["id"]; 150 | $info->state = $info->result["state"]; 151 | 152 | // Move to the live queue. 153 | $data = true; 154 | } 155 | } 156 | 157 | break; 158 | } 159 | case "update": 160 | case "read": 161 | case "write": 162 | { 163 | if ($info->keep) 164 | { 165 | $info->result = $this->ProcessState($info->state); 166 | if ($info->result["success"] || $info->result["errorcode"] !== "no_data") $info->keep = false; 167 | 168 | if (is_callable($info->callback)) call_user_func_array($info->callback, array($key, $info->url, $info->result)); 169 | 170 | if ($mode === "update") $data = $info->keep; 171 | } 172 | 173 | break; 174 | } 175 | case "readfps": 176 | { 177 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantRead($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 178 | 179 | break; 180 | } 181 | case "writefps": 182 | { 183 | if ($info->state->webstate["httpstate"] !== false && HTTP::WantWrite($info->state->webstate["httpstate"])) $data[$key] = $info->state->webstate["httpstate"]["fp"]; 184 | 185 | break; 186 | } 187 | case "cleanup": 188 | { 189 | // When true, caller is removing. Otherwise, detaching from the queue. 190 | if ($data === true) 191 | { 192 | if (isset($info->state)) 193 | { 194 | if ($info->state->webstate["httpstate"] !== false) HTTP::ForceClose($info->state->webstate["httpstate"]); 195 | 196 | unset($info->state); 197 | } 198 | 199 | $info->keep = false; 200 | } 201 | 202 | break; 203 | } 204 | } 205 | } 206 | 207 | public function ConnectAsync($helper, $key, $callback, $url, $id = false, $timeout = false, $options = array(), $web = false) 208 | { 209 | $options["async"] = true; 210 | 211 | $info = new stdClass(); 212 | $info->init = false; 213 | $info->keep = true; 214 | $info->callback = $callback; 215 | $info->url = $url; 216 | $info->id = $id; 217 | $info->timeout = $timeout; 218 | $info->options = $options; 219 | $info->web = $web; 220 | $info->result = false; 221 | 222 | $helper->Set($key, $info, array($this, "ConnectAsync__Handler")); 223 | 224 | return array("success" => true); 225 | } 226 | 227 | public static function WRTranslate() 228 | { 229 | $args = func_get_args(); 230 | if (!count($args)) return ""; 231 | 232 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 233 | } 234 | } 235 | ?> -------------------------------------------------------------------------------- /php/support/deflate_stream.php: -------------------------------------------------------------------------------- 1 | open = false; 13 | } 14 | 15 | public function __destruct() 16 | { 17 | $this->Finalize(); 18 | } 19 | 20 | public static function IsSupported() 21 | { 22 | if (!is_bool(self::$supported)) 23 | { 24 | self::$supported = function_exists("stream_filter_append") && function_exists("stream_filter_remove") && function_exists("gzcompress"); 25 | if (self::$supported) 26 | { 27 | $data = self::Compress("test"); 28 | if ($data === false || $data === "") self::$supported = false; 29 | else 30 | { 31 | $data = self::Uncompress($data); 32 | if ($data === false || $data !== "test") self::$supported = false; 33 | } 34 | } 35 | } 36 | 37 | return self::$supported; 38 | } 39 | 40 | public static function Compress($data, $compresslevel = -1, $options = array()) 41 | { 42 | $ds = new DeflateStream; 43 | if (!$ds->Init("wb", $compresslevel, $options)) return false; 44 | if (!$ds->Write($data)) return false; 45 | if (!$ds->Finalize()) return false; 46 | $data = $ds->Read(); 47 | 48 | return $data; 49 | } 50 | 51 | public static function Uncompress($data, $options = array("type" => "auto")) 52 | { 53 | $ds = new DeflateStream; 54 | if (!$ds->Init("rb", -1, $options)) return false; 55 | if (!$ds->Write($data)) return false; 56 | if (!$ds->Finalize()) return false; 57 | $data = $ds->Read(); 58 | 59 | return $data; 60 | } 61 | 62 | public function Init($mode, $compresslevel = -1, $options = array()) 63 | { 64 | if ($mode !== "rb" && $mode !== "wb") return false; 65 | if ($this->open) $this->Finalize(); 66 | 67 | $this->fp = fopen("php://memory", "w+b"); 68 | if ($this->fp === false) return false; 69 | $this->compress = ($mode == "wb"); 70 | if (!isset($options["type"])) $options["type"] = "rfc1951"; 71 | 72 | if ($options["type"] == "rfc1950") $options["type"] = "zlib"; 73 | else if ($options["type"] == "rfc1952") $options["type"] = "gzip"; 74 | 75 | if ($options["type"] != "zlib" && $options["type"] != "gzip" && ($this->compress || $options["type"] != "auto")) $options["type"] = "raw"; 76 | $this->options = $options; 77 | 78 | // Add the deflate filter. 79 | if ($this->compress) $this->filter = stream_filter_append($this->fp, "zlib.deflate", STREAM_FILTER_WRITE, $compresslevel); 80 | else $this->filter = stream_filter_append($this->fp, "zlib.inflate", STREAM_FILTER_READ); 81 | 82 | $this->open = true; 83 | $this->indata = ""; 84 | $this->outdata = ""; 85 | 86 | if ($this->compress) 87 | { 88 | if ($this->options["type"] == "zlib") 89 | { 90 | $this->outdata .= "\x78\x9C"; 91 | $this->options["a"] = 1; 92 | $this->options["b"] = 0; 93 | } 94 | else if ($this->options["type"] == "gzip") 95 | { 96 | if (!class_exists("CRC32Stream", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/crc32_stream.php"; 97 | 98 | $this->options["crc32"] = new CRC32Stream(); 99 | $this->options["crc32"]->Init(); 100 | $this->options["bytes"] = 0; 101 | 102 | $this->outdata .= "\x1F\x8B\x08"; 103 | $flags = 0; 104 | if (isset($this->options["filename"])) $flags |= 0x08; 105 | if (isset($this->options["comment"])) $flags |= 0x10; 106 | $this->outdata .= chr($flags); 107 | $this->outdata .= "\x00\x00\x00\x00"; 108 | $this->outdata .= "\x00"; 109 | $this->outdata .= "\x03"; 110 | 111 | if (isset($this->options["filename"])) $this->outdata .= str_replace("\x00", " ", $this->options["filename"]) . "\x00"; 112 | if (isset($this->options["comment"])) $this->outdata .= str_replace("\x00", " ", $this->options["comment"]) . "\x00"; 113 | } 114 | } 115 | else 116 | { 117 | $this->options["header"] = false; 118 | } 119 | 120 | return true; 121 | } 122 | 123 | public function Read() 124 | { 125 | $result = $this->outdata; 126 | $this->outdata = ""; 127 | 128 | return $result; 129 | } 130 | 131 | public function Write($data) 132 | { 133 | if (!$this->open) return false; 134 | 135 | if ($this->compress) 136 | { 137 | if ($this->options["type"] == "zlib") 138 | { 139 | // Adler-32. 140 | $y = strlen($data); 141 | for ($x = 0; $x < $y; $x++) 142 | { 143 | $this->options["a"] = ($this->options["a"] + ord($data[$x])) % 65521; 144 | $this->options["b"] = ($this->options["b"] + $this->options["a"]) % 65521; 145 | } 146 | } 147 | else if ($this->options["type"] == "gzip") 148 | { 149 | $this->options["crc32"]->AddData($data); 150 | $this->options["bytes"] = $this->ADD32($this->options["bytes"], strlen($data)); 151 | } 152 | 153 | $this->indata .= $data; 154 | while (strlen($this->indata) >= 65536) 155 | { 156 | fwrite($this->fp, substr($this->indata, 0, 65536)); 157 | $this->indata = substr($this->indata, 65536); 158 | 159 | $this->ProcessOutput(); 160 | } 161 | } 162 | else 163 | { 164 | $this->indata .= $data; 165 | $this->ProcessInput(); 166 | } 167 | 168 | return true; 169 | } 170 | 171 | // Finalizes the stream. 172 | public function Finalize() 173 | { 174 | if (!$this->open) return false; 175 | 176 | if (!$this->compress) $this->ProcessInput(true); 177 | 178 | if (strlen($this->indata) > 0) 179 | { 180 | fwrite($this->fp, $this->indata); 181 | $this->indata = ""; 182 | } 183 | 184 | // Removing the filter pushes the last buffer into the stream. 185 | stream_filter_remove($this->filter); 186 | $this->filter = false; 187 | 188 | $this->ProcessOutput(); 189 | 190 | fclose($this->fp); 191 | 192 | if ($this->compress) 193 | { 194 | if ($this->options["type"] == "zlib") $this->outdata .= pack("N", $this->SHL32($this->options["b"], 16) | $this->options["a"]); 195 | else if ($this->options["type"] == "gzip") $this->outdata .= pack("V", $this->options["crc32"]->Finalize()) . pack("V", $this->options["bytes"]); 196 | } 197 | 198 | $this->open = false; 199 | 200 | return true; 201 | } 202 | 203 | private function ProcessOutput() 204 | { 205 | rewind($this->fp); 206 | 207 | // Hack! Because ftell() on a stream with a filter is still broken even under the latest PHP a mere 11 years later. 208 | // See: https://bugs.php.net/bug.php?id=49874 209 | ob_start(); 210 | fpassthru($this->fp); 211 | $this->outdata .= ob_get_contents(); 212 | ob_end_clean(); 213 | 214 | rewind($this->fp); 215 | ftruncate($this->fp, 0); 216 | } 217 | 218 | private function ProcessInput($final = false) 219 | { 220 | // Automatically determine the type of data based on the header signature. 221 | if ($this->options["type"] == "auto") 222 | { 223 | if (strlen($this->indata) >= 3) 224 | { 225 | $zlibtest = unpack("n", substr($this->indata, 0, 2)); 226 | 227 | if (substr($this->indata, 0, 3) === "\x1F\x8B\x08") $this->options["type"] = "gzip"; 228 | else if ((ord($this->indata[0]) & 0x0F) == 8 && ((ord($this->indata[0]) & 0xF0) >> 4) < 8 && $zlibtest[1] % 31 == 0) $this->options["type"] = "zlib"; 229 | else $this->options["type"] = "raw"; 230 | } 231 | else if ($final) $this->options["type"] = "raw"; 232 | } 233 | 234 | if ($this->options["type"] == "gzip") 235 | { 236 | if (!$this->options["header"]) 237 | { 238 | if (strlen($this->indata) >= 10) 239 | { 240 | $idcm = substr($this->indata, 0, 3); 241 | $flg = ord($this->indata[3]); 242 | 243 | if ($idcm !== "\x1F\x8B\x08") $this->options["type"] = "ignore"; 244 | else 245 | { 246 | // Calculate the number of bytes to skip. If flags are set, the size can be dynamic. 247 | $size = 10; 248 | $y = strlen($this->indata); 249 | 250 | // FLG.FEXTRA 251 | if ($size && ($flg & 0x04)) 252 | { 253 | if ($size + 2 >= $y) $size = 0; 254 | else 255 | { 256 | $xlen = unpack("v", substr($this->indata, $size, 2)); 257 | $size = ($size + 2 + $xlen <= $y ? $size + 2 + $xlen : 0); 258 | } 259 | } 260 | 261 | // FLG.FNAME 262 | if ($size && ($flg & 0x08)) 263 | { 264 | $pos = strpos($this->indata, "\x00", $size); 265 | $size = ($pos !== false ? $pos + 1 : 0); 266 | } 267 | 268 | // FLG.FCOMMENT 269 | if ($size && ($flg & 0x10)) 270 | { 271 | $pos = strpos($this->indata, "\x00", $size); 272 | $size = ($pos !== false ? $pos + 1 : 0); 273 | } 274 | 275 | // FLG.FHCRC 276 | if ($size && ($flg & 0x02)) $size = ($size + 2 <= $y ? $size + 2 : 0); 277 | 278 | if ($size) 279 | { 280 | $this->indata = substr($this->indata, $size); 281 | $this->options["header"] = true; 282 | } 283 | } 284 | } 285 | } 286 | 287 | if ($this->options["header"] && strlen($this->indata) > 8) 288 | { 289 | fwrite($this->fp, substr($this->indata, 0, -8)); 290 | $this->indata = substr($this->indata, -8); 291 | 292 | $this->ProcessOutput(); 293 | } 294 | 295 | if ($final) $this->indata = ""; 296 | } 297 | else if ($this->options["type"] == "zlib") 298 | { 299 | if (!$this->options["header"]) 300 | { 301 | if (strlen($this->indata) >= 2) 302 | { 303 | $cmf = ord($this->indata[0]); 304 | $flg = ord($this->indata[1]); 305 | $cm = $cmf & 0x0F; 306 | $cinfo = ($cmf & 0xF0) >> 4; 307 | 308 | // Compression method 'deflate' ($cm = 8), window size - 8 ($cinfo < 8), no preset dictionaries ($flg bit 5), checksum validates. 309 | if ($cm != 8 || $cinfo > 7 || ($flg & 0x20) || (($cmf << 8 | $flg) % 31) != 0) $this->options["type"] = "ignore"; 310 | else 311 | { 312 | $this->indata = substr($this->indata, 2); 313 | $this->options["header"] = true; 314 | } 315 | } 316 | } 317 | 318 | if ($this->options["header"] && strlen($this->indata) > 4) 319 | { 320 | fwrite($this->fp, substr($this->indata, 0, -4)); 321 | $this->indata = substr($this->indata, -4); 322 | 323 | $this->ProcessOutput(); 324 | } 325 | 326 | if ($final) $this->indata = ""; 327 | } 328 | 329 | if ($this->options["type"] == "raw") 330 | { 331 | fwrite($this->fp, $this->indata); 332 | $this->indata = ""; 333 | 334 | $this->ProcessOutput(); 335 | } 336 | 337 | // Only set when an unrecoverable header error has occurred for gzip or zlib. 338 | if ($this->options["type"] == "ignore") $this->indata = ""; 339 | } 340 | 341 | private function SHL32($num, $bits) 342 | { 343 | if ($bits < 0) $bits = 0; 344 | 345 | return $this->LIM32((int)$num << $bits); 346 | } 347 | 348 | private function LIM32($num) 349 | { 350 | return (int)((int)$num & 0xFFFFFFFF); 351 | } 352 | 353 | private function ADD32($num, $num2) 354 | { 355 | $num = (int)$num; 356 | $num2 = (int)$num2; 357 | $add = ((($num >> 30) & 0x03) + (($num2 >> 30) & 0x03)); 358 | $num = ((int)($num & 0x3FFFFFFF) + (int)($num2 & 0x3FFFFFFF)); 359 | if ($num & 0x40000000) $add++; 360 | $num = (int)(($num & 0x3FFFFFFF) | (($add & 0x03) << 30)); 361 | 362 | return $num; 363 | } 364 | } 365 | ?> -------------------------------------------------------------------------------- /php/support/sdk_barebones_cms_lite.php: -------------------------------------------------------------------------------- 1 | $val) 10 | { 11 | if (!isset($info[$key])) $info[$key] = $val; 12 | } 13 | 14 | return $info; 15 | } 16 | 17 | public static function NormalizeAsset($info) 18 | { 19 | $defaults = array( 20 | "type" => "", "langinfo" => array(), "publish" => 0, "unpublish" => 0, 21 | "tags" => array(), "lastupdated" => 0, "lastupdatedby" => "", "lastip" => "", 22 | "files" => array(), "filesinfo" => array(), "protected" => array(), "uuid" => "" 23 | ); 24 | 25 | return self::ProcessInfoDefaults($info, $defaults); 26 | } 27 | 28 | public static function GetPreferredLanguage($acceptlangs, $defaultlang) 29 | { 30 | $langs = array(); 31 | $langs2 = array(); 32 | $acceptlangs = explode(",", strtolower($acceptlangs)); 33 | foreach ($acceptlangs as $lang) 34 | { 35 | $lang = trim($lang); 36 | $pos = strpos($lang, ";"); 37 | if ($pos === false) $q = 100; 38 | else 39 | { 40 | $q = trim(substr($lang, $pos + 1)); 41 | $lang = substr($lang, 0, $pos); 42 | 43 | if (substr($q, 0, 2) == "q=") $q = (int)(trim(substr($q, 2)) * 100); 44 | } 45 | 46 | $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", trim($lang)))); 47 | 48 | if ($lang !== "" && !isset($langs2[$lang])) 49 | { 50 | while (isset($langs[$q])) $q--; 51 | 52 | $langs[$q] = $lang; 53 | $langs2[$lang] = true; 54 | } 55 | } 56 | krsort($langs); 57 | 58 | // Look for an exact match in language preference order. 59 | foreach ($langs as $lang) 60 | { 61 | return $lang; 62 | } 63 | 64 | return $defaultlang; 65 | } 66 | 67 | public static function GetPreferredAssetLanguage($asset, $acceptlangs, $defaultlangs, $fallbackfirstlang = true, $removestrs = array()) 68 | { 69 | if (!isset($asset["langinfo"])) return false; 70 | 71 | // If the asset only has one language, then return that. 72 | if (count($asset["langinfo"]) < 2) 73 | { 74 | foreach ($asset["langinfo"] as $lang => &$info) return $lang; 75 | 76 | return false; 77 | } 78 | 79 | // Parse the list of languages from Accept-Language (e.g. Accept-Language: fr-CH, fr;q=0.9, de;q=0.7, en;q=0.8, *;q=0.5). 80 | $langs = array(); 81 | $langs2 = array(); 82 | $acceptlangs = explode(",", strtolower($acceptlangs)); 83 | foreach ($acceptlangs as $lang) 84 | { 85 | $lang = trim($lang); 86 | $pos = strpos($lang, ";"); 87 | if ($pos === false) $q = 100; 88 | else 89 | { 90 | $q = trim(substr($lang, $pos + 1)); 91 | $lang = substr($lang, 0, $pos); 92 | 93 | if (substr($q, 0, 2) == "q=") $q = (int)(trim(substr($q, 2)) * 100); 94 | } 95 | 96 | $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", trim($lang)))); 97 | 98 | // Remove strings (e.g. internal language suffixes) found in user-submitted Accept-Language strings (e.g. '-newsletter' would be removed from 'en-us-newsletter'). 99 | $found = false; 100 | foreach ($removestrs as $str) 101 | { 102 | if (stripos($lang, $str) !== false) 103 | { 104 | $lang = str_ireplace($str, "", $lang); 105 | 106 | $found = true; 107 | } 108 | } 109 | 110 | if ($found) $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", $lang))); 111 | 112 | if ($lang !== "" && !isset($langs2[$lang])) 113 | { 114 | while (isset($langs[$q])) $q--; 115 | 116 | $langs[$q] = $lang; 117 | $langs2[$lang] = true; 118 | } 119 | } 120 | krsort($langs); 121 | 122 | // Look for an exact match in language preference order. 123 | foreach ($langs as $lang) 124 | { 125 | if (isset($asset["langinfo"][$lang])) return $lang; 126 | } 127 | 128 | // Look for a partial hyphenated match (e.g. 'en' is a 66.67% partial match for 'en-us' - that is, two out of three segments match). 129 | $partiallang = false; 130 | $partialpercent = 0; 131 | $langs2 = array(); 132 | foreach ($asset["langinfo"] as $lang2 => &$info) $langs2[$lang2] = explode("-", $lang2); 133 | foreach ($langs as $lang) 134 | { 135 | $words = explode("-", $lang); 136 | $numwords = count($words); 137 | 138 | foreach ($langs2 as $lang2 => $words2) 139 | { 140 | $y = min($numwords, count($words2)); 141 | for ($x = 0; $x < $y && $words[$x] === $words2[$x]; $x++) 142 | { 143 | } 144 | 145 | if ($x) 146 | { 147 | $percent = ($x * 2) / ($numwords + count($words2)); 148 | if ($partialpercent < $percent) 149 | { 150 | $partiallang = $lang2; 151 | $partialpercent = $percent; 152 | } 153 | } 154 | } 155 | } 156 | 157 | if ($partiallang !== false) return $partiallang; 158 | 159 | // A default language, if it exists. 160 | if (!is_array($defaultlangs)) $defaultlangs = array($defaultlangs); 161 | foreach ($defaultlangs as $lang) 162 | { 163 | if (isset($asset["langinfo"][$lang])) return $lang; 164 | } 165 | 166 | // First available option. 167 | if ($fallbackfirstlang) 168 | { 169 | foreach ($asset["langinfo"] as $lang => &$info) return $lang; 170 | } 171 | 172 | return false; 173 | } 174 | 175 | public static function GetPreferredTag($tags, $prefix, $overrideprefix = false) 176 | { 177 | $preftag = false; 178 | $y = strlen($prefix); 179 | if ($overrideprefix !== false) $y2 = strlen($overrideprefix); 180 | foreach ($tags as $tag) 181 | { 182 | if ($preftag === false && !strncmp($tag, $prefix, $y)) $preftag = $tag; 183 | else if ($overrideprefix !== false && !strncmp($tag, $overrideprefix, $y2)) $preftag = substr($tag, 1); 184 | } 185 | 186 | return $preftag; 187 | } 188 | 189 | // Determines if the user has supplied a valid content refresh token. 190 | public static function CanRefreshContent($validtoken, $requestkey = "refresh") 191 | { 192 | // If PHP sesssion are enabled and the user appears to have a valid session or request, check that first. 193 | unset($_COOKIE["bb_valid"]); 194 | if (session_status() !== PHP_SESSION_DISABLED) 195 | { 196 | if (isset($_COOKIE["bb"]) || (isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken)) 197 | { 198 | // Close an existing session. 199 | $currsession = (session_status() === PHP_SESSION_ACTIVE); 200 | if ($currsession) @session_write_close(); 201 | 202 | // Switch to the 'bb' session. 203 | $prevname = @session_name("bb"); 204 | @session_start(); 205 | 206 | $_SESSION["ts"] = time(); 207 | 208 | if (!isset($_SESSION["bb_cms_refresh_keys"])) $_SESSION["bb_cms_refresh_keys"] = array(); 209 | if (!isset($_SESSION["bb_cms_refresh_keys"][$validtoken]) && ((isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken))) $_SESSION["bb_cms_refresh_keys"][$validtoken] = true; 210 | $valid = isset($_SESSION["bb_cms_refresh_keys"][$validtoken]); 211 | 212 | @session_write_close(); 213 | @session_name($prevname); 214 | 215 | // Restore previous session (if any). 216 | if ($currsession) @session_start(); 217 | 218 | // Stop processing when the request is simply a heartbeat. 219 | if (isset($_POST["bb_heartbeat"])) exit(); 220 | 221 | if ($valid) $_COOKIE["bb_valid"] = true; 222 | 223 | return $valid; 224 | } 225 | } 226 | 227 | // Fallback to using browser cookies. Only supports one active refresh token at a time. 228 | if ((isset($_COOKIE[$requestkey]) && $_COOKIE[$requestkey] === $validtoken) || (isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken)) 229 | { 230 | if (!isset($_COOKIE[$requestkey]) || $_COOKIE[$requestkey] !== $validtoken) @setcookie($requestkey, $validtoken, 0, "", "", false, true); 231 | 232 | return true; 233 | } 234 | 235 | return false; 236 | } 237 | 238 | // Calculates the full path and filename to cached assets. 239 | public static function GetCachedAssetsFilename($contentdir, $type, $key) 240 | { 241 | $key = md5(json_encode($key, JSON_UNESCAPED_SLASHES)); 242 | $filename = $contentdir . "/" . $type . "/" . substr($key, 0, 2) . "/assets_" . $key . ".dat"; 243 | 244 | return $filename; 245 | } 246 | 247 | // Loads cached assets into memory. Pays heed to publish/unpublish times. 248 | public static function LoadCachedAssets($contentdirfilename, $type = false, $key = false) 249 | { 250 | if ($type === false || $key === false) $filename = $contentdirfilename; 251 | else $filename = self::GetCachedAssetsFilename($contentdirfilename, $type, $key); 252 | 253 | if (!file_exists($filename)) return array(); 254 | 255 | $data = @json_decode(file_get_contents($filename), true); 256 | if (!is_array($data)) return array(); 257 | $assets = $data["assets"]; 258 | unset($data); 259 | 260 | $ts = time(); 261 | $result = array(); 262 | foreach ($assets as $num => $asset) 263 | { 264 | $asset = self::NormalizeAsset($asset); 265 | unset($asset["lastupdatedby"]); 266 | unset($asset["lastip"]); 267 | unset($asset["protected"]); 268 | 269 | if ($asset["publish"] > 0 && $asset["publish"] <= $ts && ($asset["unpublish"] == 0 || $asset["unpublish"] > $ts)) $result[] = $asset; 270 | } 271 | 272 | return $result; 273 | } 274 | } 275 | 276 | class Request 277 | { 278 | protected static $hostcache; 279 | 280 | protected static function ProcessSingleInput($data) 281 | { 282 | foreach ($data as $key => $val) 283 | { 284 | if (is_string($val)) $_REQUEST[$key] = trim($val); 285 | else if (is_array($val)) 286 | { 287 | $_REQUEST[$key] = array(); 288 | foreach ($val as $key2 => $val2) $_REQUEST[$key][$key2] = (is_string($val2) ? trim($val2) : $val2); 289 | } 290 | else $_REQUEST[$key] = $val; 291 | } 292 | } 293 | 294 | // Cleans up all PHP input issues so that $_REQUEST may be used as expected. 295 | public static function Normalize() 296 | { 297 | self::ProcessSingleInput($_COOKIE); 298 | self::ProcessSingleInput($_GET); 299 | self::ProcessSingleInput($_POST); 300 | } 301 | 302 | public static function IsSSL() 303 | { 304 | return ((isset($_SERVER["HTTPS"]) && ($_SERVER["HTTPS"] == "on" || $_SERVER["HTTPS"] == "1")) || (isset($_SERVER["SERVER_PORT"]) && $_SERVER["SERVER_PORT"] == "443") || (isset($_SERVER["REQUEST_URI"]) && str_replace("\\", "/", strtolower(substr($_SERVER["REQUEST_URI"], 0, 8))) == "https://")); 305 | } 306 | 307 | // Returns 'http[s]://www.something.com[:port]' based on the current page request. 308 | public static function GetHost($protocol = "") 309 | { 310 | $protocol = strtolower($protocol); 311 | $ssl = ($protocol == "https" || ($protocol == "" && self::IsSSL())); 312 | if ($protocol == "") $type = "def"; 313 | else if ($ssl) $type = "https"; 314 | else $type = "http"; 315 | 316 | if (!isset(self::$hostcache)) self::$hostcache = array(); 317 | if (isset(self::$hostcache[$type])) return self::$hostcache[$type]; 318 | 319 | $url = "http" . ($ssl ? "s" : "") . "://"; 320 | 321 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 322 | $pos = strpos($str, "?"); 323 | if ($pos !== false) $str = substr($str, 0, $pos); 324 | $str2 = strtolower($str); 325 | if (substr($str2, 0, 7) == "http://") 326 | { 327 | $pos = strpos($str, "/", 7); 328 | if ($pos === false) $str = ""; 329 | else $str = substr($str, 7, $pos); 330 | } 331 | else if (substr($str2, 0, 8) == "https://") 332 | { 333 | $pos = strpos($str, "/", 8); 334 | if ($pos === false) $str = ""; 335 | else $str = substr($str, 8, $pos); 336 | } 337 | else $str = ""; 338 | 339 | if ($str != "") $host = $str; 340 | else if (isset($_SERVER["HTTP_HOST"])) $host = $_SERVER["HTTP_HOST"]; 341 | else $host = $_SERVER["SERVER_NAME"] . ":" . (int)$_SERVER["SERVER_PORT"]; 342 | 343 | $pos = strpos($host, ":"); 344 | if ($pos === false) $port = 0; 345 | else 346 | { 347 | $port = (int)substr($host, $pos + 1); 348 | $host = substr($host, 0, $pos); 349 | } 350 | if ($port < 1 || $port > 65535) $port = ($ssl ? 443 : 80); 351 | $url .= preg_replace('/[^a-z0-9.\-]/', "", strtolower($host)); 352 | if ($protocol == "" && ((!$ssl && $port != 80) || ($ssl && $port != 443))) $url .= ":" . $port; 353 | else if ($protocol == "http" && !$ssl && $port != 80) $url .= ":" . $port; 354 | else if ($protocol == "https" && $ssl && $port != 443) $url .= ":" . $port; 355 | 356 | self::$hostcache[$type] = $url; 357 | 358 | return $url; 359 | } 360 | 361 | public static function GetURLBase() 362 | { 363 | $str = (isset($_SERVER["REQUEST_URI"]) ? str_replace("\\", "/", $_SERVER["REQUEST_URI"]) : "/"); 364 | $pos = strpos($str, "?"); 365 | if ($pos !== false) $str = substr($str, 0, $pos); 366 | if (strncasecmp($str, "http://", 7) == 0 || strncasecmp($str, "https://", 8) == 0) 367 | { 368 | $pos = strpos($str, "/", 8); 369 | if ($pos === false) $str = "/"; 370 | else $str = substr($str, $pos); 371 | } 372 | 373 | return $str; 374 | } 375 | 376 | public static function GetFullURLBase($protocol = "") 377 | { 378 | return self::GetHost($protocol) . self::GetURLBase(); 379 | } 380 | 381 | public static function PrependHost($url, $protocol = "") 382 | { 383 | // Handle protocol-only. 384 | if (strncmp($url, "//", 2) == 0) 385 | { 386 | $host = self::GetHost($protocol); 387 | $pos = strpos($host, ":"); 388 | if ($pos === false) return $url; 389 | 390 | return substr($host, 0, $pos + 1) . $url; 391 | } 392 | 393 | if (strpos($url, ":") !== false) return $url; 394 | 395 | // Handle relative paths. 396 | if ($url === "" || $url[0] !== "/") return rtrim(self::GetFullURLBase($protocol), "/") . "/" . $url; 397 | 398 | // Handle '/path/'. 399 | $host = self::GetHost($protocol); 400 | 401 | return $host . $url; 402 | } 403 | } 404 | ?> -------------------------------------------------------------------------------- /php/support/utf_utils.php: -------------------------------------------------------------------------------- 1 | = 0x0300 && $val <= 0x036F) || ($val >= 0x1DC0 && $val <= 0x1DFF) || ($val >= 0x20D0 && $val <= 0x20FF) || ($val >= 0xFE20 && $val <= 0xFE2F)); 21 | } 22 | 23 | public static function Convert($data, $srctype, $desttype) 24 | { 25 | $arr = is_array($data); 26 | if ($arr) $srctype = self::UTF32_ARRAY; 27 | $x = 0; 28 | $y = ($arr ? count($data) : strlen($data)); 29 | $result = ($desttype === self::UTF32_ARRAY ? array() : ""); 30 | if (!$arr && $srctype === self::UTF32_ARRAY) return $result; 31 | 32 | $first = true; 33 | 34 | if ($srctype === self::UTF8_BOM) 35 | { 36 | if (substr($data, 0, 3) === "\xEF\xBB\xBF") $x = 3; 37 | 38 | $srctype = self::UTF8; 39 | } 40 | 41 | if ($srctype === self::UTF16_BOM) 42 | { 43 | if (substr($data, 0, 2) === "\xFE\xFF") 44 | { 45 | $srctype = self::UTF16_BE; 46 | $x = 2; 47 | } 48 | else if (substr($data, 0, 2) === "\xFF\xFE") 49 | { 50 | $srctype = self::UTF16_LE; 51 | $x = 2; 52 | } 53 | else 54 | { 55 | $srctype = self::UTF16_LE; 56 | } 57 | } 58 | 59 | if ($srctype === self::UTF32_BOM) 60 | { 61 | if (substr($data, 0, 4) === "\x00\x00\xFE\xFF") 62 | { 63 | $srctype = self::UTF32_BE; 64 | $x = 4; 65 | } 66 | else if (substr($data, 0, 4) === "\xFF\xFE\x00\x00") 67 | { 68 | $srctype = self::UTF32_LE; 69 | $x = 4; 70 | } 71 | else 72 | { 73 | $srctype = self::UTF32_LE; 74 | } 75 | } 76 | 77 | while ($x < $y) 78 | { 79 | // Read the next valid code point. 80 | $val = false; 81 | 82 | switch ($srctype) 83 | { 84 | case self::UTF8: 85 | { 86 | $tempchr = ord($data[$x]); 87 | if ($tempchr <= 0x7F) 88 | { 89 | $val = $tempchr; 90 | $x++; 91 | } 92 | else if ($tempchr < 0xC2) $x++; 93 | else 94 | { 95 | $left = $y - $x; 96 | if ($left < 2) $x++; 97 | else 98 | { 99 | $tempchr2 = ord($data[$x + 1]); 100 | 101 | if (($tempchr >= 0xC2 && $tempchr <= 0xDF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) 102 | { 103 | $val = (($tempchr & 0x1F) << 6) | ($tempchr2 & 0x3F); 104 | $x += 2; 105 | } 106 | else if ($left < 3) $x++; 107 | else 108 | { 109 | $tempchr3 = ord($data[$x + 2]); 110 | 111 | if ($tempchr3 < 0x80 || $tempchr3 > 0xBF) $x++; 112 | else 113 | { 114 | if (($tempchr == 0xE0 && ($tempchr2 >= 0xA0 && $tempchr2 <= 0xBF)) || ((($tempchr >= 0xE1 && $tempchr <= 0xEC) || $tempchr == 0xEE || $tempchr == 0xEF) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xED && ($tempchr2 >= 0x80 && $tempchr2 <= 0x9F))) 115 | { 116 | $val = (($tempchr & 0x0F) << 12) | (($tempchr2 & 0x3F) << 6) | ($tempchr3 & 0x3F); 117 | $x += 3; 118 | } 119 | else if ($left < 4) $x++; 120 | else 121 | { 122 | $tempchr4 = ord($data[$x + 3]); 123 | 124 | if ($tempchr4 < 0x80 || $tempchr4 > 0xBF) $x++; 125 | else if (($tempchr == 0xF0 && ($tempchr2 >= 0x90 && $tempchr2 <= 0xBF)) || (($tempchr >= 0xF1 && $tempchr <= 0xF3) && ($tempchr2 >= 0x80 && $tempchr2 <= 0xBF)) || ($tempchr == 0xF4 && ($tempchr2 >= 0x80 && $tempchr2 <= 0x8F))) 126 | { 127 | $val = (($tempchr & 0x07) << 18) | (($tempchr2 & 0x3F) << 12) | (($tempchr3 & 0x3F) << 6) | ($tempchr4 & 0x3F); 128 | $x += 4; 129 | } 130 | else 131 | { 132 | $x++; 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | break; 141 | } 142 | case self::UTF16_LE: 143 | { 144 | if ($x + 1 >= $y) $x = $y; 145 | else 146 | { 147 | $val = unpack("v", substr($data, $x, 2))[1]; 148 | $x += 2; 149 | 150 | if ($val >= 0xD800 && $val <= 0xDBFF) 151 | { 152 | if ($x + 1 >= $y) 153 | { 154 | $x = $y; 155 | $val = false; 156 | } 157 | else 158 | { 159 | $val2 = unpack("v", substr($data, $x, 2))[1]; 160 | 161 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 162 | else 163 | { 164 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 165 | $x += 2; 166 | } 167 | } 168 | } 169 | } 170 | 171 | break; 172 | } 173 | case self::UTF16_BE: 174 | { 175 | if ($x + 1 >= $y) $x = $y; 176 | else 177 | { 178 | $val = unpack("n", substr($data, $x, 2))[1]; 179 | $x += 2; 180 | 181 | if ($val >= 0xD800 && $val <= 0xDBFF) 182 | { 183 | if ($x + 1 >= $y) 184 | { 185 | $x = $y; 186 | $val = false; 187 | } 188 | else 189 | { 190 | $val2 = unpack("n", substr($data, $x, 2))[1]; 191 | 192 | if ($val2 < 0xDC00 || $val2 > 0xDFFF) $val = false; 193 | else 194 | { 195 | $val = ((($val - 0xD800) << 10) | ($val2 - 0xDC00)) + 0x10000; 196 | $x += 2; 197 | } 198 | } 199 | } 200 | } 201 | 202 | break; 203 | } 204 | case self::UTF32_LE: 205 | { 206 | if ($x + 3 >= $y) $x = $y; 207 | else 208 | { 209 | $val = unpack("V", substr($data, $x, 4))[1]; 210 | $x += 4; 211 | } 212 | 213 | break; 214 | } 215 | case self::UTF32_BE: 216 | { 217 | if ($x + 3 >= $y) $x = $y; 218 | else 219 | { 220 | $val = unpack("N", substr($data, $x, 4))[1]; 221 | $x += 4; 222 | } 223 | 224 | break; 225 | } 226 | case self::UTF32_ARRAY: 227 | { 228 | $val = (int)$data[$x]; 229 | $x++; 230 | 231 | break; 232 | } 233 | default: $x = $y; break; 234 | } 235 | 236 | // Make sure it is a valid Unicode value. 237 | // 0xD800-0xDFFF are for UTF-16 surrogate pairs. Invalid characters. 238 | // 0xFDD0-0xFDEF are non-characters. 239 | // 0x*FFFE and 0x*FFFF are reserved. 240 | // The largest possible character is 0x10FFFF. 241 | // First character can't be a combining code point. 242 | if ($val !== false && !($val < 0 || ($val >= 0xD800 && $val <= 0xDFFF) || ($val >= 0xFDD0 && $val <= 0xFDEF) || ($val & 0xFFFE) == 0xFFFE || $val > 0x10FFFF || ($first && self::IsCombiningCodePoint($val)))) 243 | { 244 | if ($first) 245 | { 246 | if ($desttype === self::UTF8_BOM) 247 | { 248 | $result .= "\xEF\xBB\xBF"; 249 | 250 | $desttype = self::UTF8; 251 | } 252 | 253 | if ($desttype === self::UTF16_BOM) 254 | { 255 | $result .= "\xFF\xFE"; 256 | 257 | $desttype = self::UTF16_LE; 258 | } 259 | 260 | if ($srctype === self::UTF32_BOM) 261 | { 262 | $result .= "\xFF\xFE\x00\x00"; 263 | 264 | $desttype = self::UTF32_LE; 265 | } 266 | 267 | $first = false; 268 | } 269 | 270 | switch ($desttype) 271 | { 272 | case self::UTF8: 273 | { 274 | if ($val <= 0x7F) $result .= chr($val); 275 | else if ($val <= 0x7FF) $result .= chr(0xC0 | ($val >> 6)) . chr(0x80 | ($val & 0x3F)); 276 | else if ($val <= 0xFFFF) $result .= chr(0xE0 | ($val >> 12)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 277 | else if ($val <= 0x10FFFF) $result .= chr(0xF0 | ($val >> 18)) . chr(0x80 | (($val >> 12) & 0x3F)) . chr(0x80 | (($val >> 6) & 0x3F)) . chr(0x80 | ($val & 0x3F)); 278 | 279 | break; 280 | } 281 | case self::UTF16_LE: 282 | { 283 | if ($val <= 0xFFFF) $result .= pack("v", $val); 284 | else 285 | { 286 | $val -= 0x10000; 287 | $result .= pack("v", ((($val >> 10) & 0x3FF) + 0xD800)); 288 | $result .= pack("v", (($val & 0x3FF) + 0xDC00)); 289 | } 290 | 291 | break; 292 | } 293 | case self::UTF16_BE: 294 | { 295 | if ($val <= 0xFFFF) $result .= pack("n", $val); 296 | else 297 | { 298 | $val -= 0x10000; 299 | $result .= pack("n", ((($val >> 10) & 0x3FF) + 0xD800)); 300 | $result .= pack("n", (($val & 0x3FF) + 0xDC00)); 301 | } 302 | 303 | break; 304 | } 305 | case self::UTF32_LE: 306 | { 307 | $result .= pack("V", $val); 308 | 309 | break; 310 | } 311 | case self::UTF32_BE: 312 | { 313 | $result .= pack("N", $val); 314 | 315 | break; 316 | } 317 | case self::UTF32_ARRAY: 318 | { 319 | $result[] = $val; 320 | 321 | break; 322 | } 323 | default: $x = $y; break; 324 | } 325 | } 326 | } 327 | 328 | return $result; 329 | } 330 | 331 | 332 | protected const PUNYCODE_BASE = 36; 333 | protected const PUNYCODE_TMIN = 1; 334 | protected const PUNYCODE_TMAX = 26; 335 | protected const PUNYCODE_SKEW = 38; 336 | protected const PUNYCODE_DAMP = 700; 337 | protected const PUNYCODE_INITIAL_BIAS = 72; 338 | protected const PUNYCODE_INITIAL_N = 0x80; 339 | protected const PUNYCODE_DIGIT_MAP = "abcdefghijklmnopqrstuvwxyz0123456789"; 340 | 341 | public static function ConvertToPunycode($domain) 342 | { 343 | // Reject invalid domain name lengths. 344 | if (strlen($domain) > 255) return false; 345 | 346 | $parts = explode(".", $domain); 347 | 348 | foreach ($parts as $num => $part) 349 | { 350 | // Reject invalid label lengths. 351 | $y = strlen($part); 352 | if ($y > 63) return false; 353 | 354 | // Skip already encoded portions. 355 | if (substr($part, 0, 4) === "xn--") continue; 356 | 357 | // Convert UTF-8 to UTF-32 code points. 358 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 359 | 360 | // Handle ASCII code points. 361 | $part2 = ""; 362 | foreach ($data as $cp) 363 | { 364 | if ($cp <= 0x7F) $part2 .= strtolower(chr($cp)); 365 | } 366 | 367 | $numhandled = strlen($part2); 368 | $y = count($data); 369 | 370 | if ($numhandled >= $y) 371 | { 372 | $parts[$num] = $part2; 373 | 374 | continue; 375 | } 376 | 377 | if ($numhandled) $part2 .= "-"; 378 | 379 | $part2 = "xn--" . $part2; 380 | 381 | if (strlen($part2) > 63) return false; 382 | 383 | $bias = self::PUNYCODE_INITIAL_BIAS; 384 | $n = self::PUNYCODE_INITIAL_N; 385 | $delta = 0; 386 | $first = true; 387 | 388 | while ($numhandled < $y) 389 | { 390 | // Find the next largest unhandled code point. 391 | $cp2 = 0x01000000; 392 | foreach ($data as $cp) 393 | { 394 | if ($cp >= $n && $cp2 > $cp) $cp2 = $cp; 395 | } 396 | 397 | // Increase delta but prevent overflow. 398 | $delta += ($cp2 - $n) * ($numhandled + 1); 399 | if ($delta < 0) return false; 400 | $n = $cp2; 401 | 402 | foreach ($data as $cp) 403 | { 404 | if ($cp < $n) 405 | { 406 | $delta++; 407 | 408 | if ($delta < 0) return false; 409 | } 410 | else if ($cp === $n) 411 | { 412 | // Calculate and encode a variable length integer from the delta. 413 | $q = $delta; 414 | $x = 0; 415 | do 416 | { 417 | $x += self::PUNYCODE_BASE; 418 | 419 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 420 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 421 | else $t = $x - $bias; 422 | 423 | if ($q < $t) break; 424 | 425 | $part2 .= self::PUNYCODE_DIGIT_MAP[$t + (($q - $t) % (self::PUNYCODE_BASE - $t))]; 426 | 427 | $q = (int)(($q - $t) / (self::PUNYCODE_BASE - $t)); 428 | 429 | if (strlen($part2) > 63) return false; 430 | } while (1); 431 | 432 | $part2 .= self::PUNYCODE_DIGIT_MAP[$q]; 433 | if (strlen($part2) > 63) return false; 434 | 435 | // Adapt bias. 436 | $numhandled++; 437 | $bias = self::InternalPunycodeAdapt($delta, $numhandled, $first); 438 | $delta = 0; 439 | $first = false; 440 | } 441 | } 442 | 443 | $delta++; 444 | $n++; 445 | } 446 | 447 | $parts[$num] = $part2; 448 | } 449 | 450 | return implode(".", $parts); 451 | } 452 | 453 | public static function ConvertFromPunycode($domain) 454 | { 455 | // Reject invalid domain name lengths. 456 | if (strlen($domain) > 255) return false; 457 | 458 | $parts = explode(".", $domain); 459 | 460 | foreach ($parts as $num => $part) 461 | { 462 | // Reject invalid label lengths. 463 | $y = strlen($part); 464 | if ($y > 63) return false; 465 | 466 | // Skip unencoded portions. 467 | if (substr($part, 0, 4) !== "xn--") continue; 468 | 469 | $part = substr($part, 4); 470 | 471 | // Convert UTF-8 to UTF-32 code points. 472 | $data = self::Convert($part, self::UTF8, self::UTF32_ARRAY); 473 | 474 | // Handle ASCII code points. 475 | $hyphen = ord("-"); 476 | for ($x = count($data); $x && $data[$x - 1] !== $hyphen; $x--); 477 | if (!$x) $data2 = array(); 478 | else 479 | { 480 | $data2 = array_splice($data, 0, $x - 1); 481 | 482 | array_shift($data); 483 | } 484 | 485 | $numhandled = count($data2); 486 | 487 | $bias = self::PUNYCODE_INITIAL_BIAS; 488 | $n = self::PUNYCODE_INITIAL_N; 489 | $delta = 0; 490 | $first = true; 491 | 492 | $pos = 0; 493 | $y = count($data); 494 | while ($pos < $y) 495 | { 496 | // Calculate and decode a delta from the variable length integer. 497 | $olddelta = $delta; 498 | $w = 1; 499 | $x = 0; 500 | do 501 | { 502 | $x += self::PUNYCODE_BASE; 503 | 504 | $cp = $data[$pos]; 505 | $pos++; 506 | 507 | if ($cp >= ord("a") && $cp <= ord("z")) $digit = $cp - ord("a"); 508 | else if ($cp >= ord("A") && $cp <= ord("Z")) $digit = $cp - ord("A"); 509 | else if ($cp >= ord("0") && $cp <= ord("9")) $digit = $cp - ord("0") + 26; 510 | else return false; 511 | 512 | $delta += $digit * $w; 513 | if ($delta < 0) return false; 514 | 515 | if ($x <= $bias) $t = self::PUNYCODE_TMIN; 516 | else if ($x >= $bias + self::PUNYCODE_TMAX) $t = self::PUNYCODE_TMAX; 517 | else $t = $x - $bias; 518 | 519 | if ($digit < $t) break; 520 | 521 | $w *= (self::PUNYCODE_BASE - $t); 522 | if ($w < 0) return false; 523 | } while (1); 524 | 525 | // Adapt bias. 526 | $numhandled++; 527 | $bias = self::InternalPunycodeAdapt($delta - $olddelta, $numhandled, $first); 528 | $first = false; 529 | 530 | // Delta was supposed to wrap around from $numhandled to 0, incrementing $n each time, so fix that now. 531 | $n += (int)($delta / $numhandled); 532 | $delta %= $numhandled; 533 | 534 | // Insert $n (the code point) at the delta position. 535 | array_splice($data2, $delta, 0, array($n)); 536 | $delta++; 537 | } 538 | 539 | $parts[$num] = self::Convert($data2, self::UTF32_ARRAY, self::UTF8); 540 | } 541 | 542 | return implode(".", $parts); 543 | } 544 | 545 | // RFC3492 adapt() function. 546 | protected static function InternalPunycodeAdapt($delta, $numpoints, $first) 547 | { 548 | $delta = ($first ? (int)($delta / self::PUNYCODE_DAMP) : $delta >> 1); 549 | $delta += (int)($delta / $numpoints); 550 | 551 | $y = self::PUNYCODE_BASE - self::PUNYCODE_TMIN; 552 | 553 | $condval = (int)(($y * self::PUNYCODE_TMAX) / 2); 554 | for ($x = 0; $delta > $condval; $x += self::PUNYCODE_BASE) $delta = (int)($delta / $y); 555 | 556 | return (int)($x + ((($y + 1) * $delta) / ($delta + self::PUNYCODE_SKEW))); 557 | } 558 | } 559 | ?> -------------------------------------------------------------------------------- /php/support/web_browser.php: -------------------------------------------------------------------------------- 1 | ResetState(); 15 | $this->SetState($prevstate); 16 | $this->html = false; 17 | } 18 | 19 | public function ResetState() 20 | { 21 | $this->data = array( 22 | "allowedprotocols" => array("http" => true, "https" => true), 23 | "allowedredirprotocols" => array("http" => true, "https" => true), 24 | "hostauths" => array(), 25 | "cookies" => array(), 26 | "referer" => "", 27 | "autoreferer" => true, 28 | "useragent" => "firefox", 29 | "followlocation" => true, 30 | "maxfollow" => 20, 31 | "extractforms" => false, 32 | "httpopts" => array(), 33 | ); 34 | } 35 | 36 | public function SetState($options = array()) 37 | { 38 | $this->data = array_merge($this->data, $options); 39 | } 40 | 41 | public function GetState() 42 | { 43 | return $this->data; 44 | } 45 | 46 | public function ProcessState(&$state) 47 | { 48 | while ($state["state"] !== "done") 49 | { 50 | switch ($state["state"]) 51 | { 52 | case "initialize": 53 | { 54 | if (!isset($this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) || !$this->data["allowedprotocols"][$state["urlinfo"]["scheme"]]) 55 | { 56 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed in '%s'.", $state["urlinfo"]["scheme"], $state["url"]), "errorcode" => "allowed_protocols"); 57 | } 58 | 59 | $filename = HTTP::ExtractFilename($state["urlinfo"]["path"]); 60 | $pos = strrpos($filename, "."); 61 | $fileext = ($pos !== false ? strtolower(substr($filename, $pos + 1)) : ""); 62 | 63 | // Set up some standard headers. 64 | $headers = array(); 65 | $profile = strtolower($state["profile"]); 66 | $tempprofile = explode("-", $profile); 67 | if (count($tempprofile) == 2) 68 | { 69 | $profile = $tempprofile[0]; 70 | $fileext = $tempprofile[1]; 71 | } 72 | if (substr($profile, 0, 2) == "ie" || ($profile == "auto" && substr($this->data["useragent"], 0, 2) == "ie")) 73 | { 74 | if ($fileext == "css") $headers["Accept"] = "text/css"; 75 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png, image/svg+xml, image/*;q=0.8, */*;q=0.5"; 76 | else if ($fileext == "js") $headers["Accept"] = "application/javascript, */*;q=0.8"; 77 | else if ($this->data["referer"] != "" || $fileext == "" || $fileext == "html" || $fileext == "xhtml" || $fileext == "xml") $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 78 | else $headers["Accept"] = "*/*"; 79 | 80 | $headers["Accept-Language"] = "en-US"; 81 | $headers["User-Agent"] = HTTP::GetUserAgent(substr($profile, 0, 2) == "ie" ? $profile : $this->data["useragent"]); 82 | } 83 | else if ($profile == "firefox" || ($profile == "auto" && $this->data["useragent"] == "firefox")) 84 | { 85 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 86 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg") $headers["Accept"] = "image/png,image/*;q=0.8,*/*;q=0.5"; 87 | else if ($fileext == "js") $headers["Accept"] = "*/*"; 88 | else $headers["Accept"] = "text/html, application/xhtml+xml, */*"; 89 | 90 | $headers["Accept-Language"] = "en-us,en;q=0.5"; 91 | $headers["Cache-Control"] = "max-age=0"; 92 | $headers["User-Agent"] = HTTP::GetUserAgent("firefox"); 93 | } 94 | else if ($profile == "opera" || ($profile == "auto" && $this->data["useragent"] == "opera")) 95 | { 96 | // Opera has the right idea: Just send the same thing regardless of the request type. 97 | $headers["Accept"] = "text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1"; 98 | $headers["Accept-Language"] = "en-US,en;q=0.9"; 99 | $headers["Cache-Control"] = "no-cache"; 100 | $headers["User-Agent"] = HTTP::GetUserAgent("opera"); 101 | } 102 | else if ($profile == "safari" || $profile == "edge" || $profile == "chrome" || ($profile == "auto" && ($this->data["useragent"] == "safari" || $this->data["useragent"] == "edge" || $this->data["useragent"] == "chrome"))) 103 | { 104 | if ($fileext == "css") $headers["Accept"] = "text/css,*/*;q=0.1"; 105 | else if ($fileext == "png" || $fileext == "jpg" || $fileext == "jpeg" || $fileext == "gif" || $fileext == "svg" || $fileext == "js") $headers["Accept"] = "*/*"; 106 | else $headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; 107 | 108 | $headers["Accept-Charset"] = "ISO-8859-1,utf-8;q=0.7,*;q=0.3"; 109 | $headers["Accept-Language"] = "en-US,en;q=0.8"; 110 | $headers["User-Agent"] = HTTP::GetUserAgent($profile == "safari" || $profile == "chrome" ? $profile : $this->data["useragent"]); 111 | } 112 | 113 | if ($this->data["referer"] != "") $headers["Referer"] = $this->data["referer"]; 114 | 115 | // Generate the final headers array. 116 | $headers = array_merge($headers, $state["httpopts"]["headers"], $state["tempoptions"]["headers"]); 117 | 118 | // Calculate the host and reverse host and remove port information. 119 | $host = (isset($headers["Host"]) ? $headers["Host"] : $state["urlinfo"]["host"]); 120 | $pos = strpos($host, "]"); 121 | if (substr($host, 0, 1) == "[" && $pos !== false) 122 | { 123 | $host = substr($host, 0, $pos + 1); 124 | } 125 | else 126 | { 127 | $pos = strpos($host, ":"); 128 | if ($pos !== false) $host = substr($host, 0, $pos); 129 | } 130 | $dothost = $host; 131 | $dothost = strtolower($dothost); 132 | if (substr($dothost, 0, 1) != ".") $dothost = "." . $dothost; 133 | $state["dothost"] = $dothost; 134 | 135 | // Append Authorization header. 136 | if (isset($headers["Authorization"])) $this->data["hostauths"][$host] = $headers["Authorization"]; 137 | else if (isset($this->data["hostauths"][$host])) $headers["Authorization"] = $this->data["hostauths"][$host]; 138 | 139 | // Append cookies and delete old, invalid cookies. 140 | $secure = ($state["urlinfo"]["scheme"] == "https"); 141 | $cookiepath = $state["urlinfo"]["path"]; 142 | if ($cookiepath == "") $cookiepath = "/"; 143 | $pos = strrpos($cookiepath, "/"); 144 | if ($pos !== false) $cookiepath = substr($cookiepath, 0, $pos + 1); 145 | $state["cookiepath"] = $cookiepath; 146 | $cookies = array(); 147 | foreach ($this->data["cookies"] as $domain => $paths) 148 | { 149 | if (strlen($dothost) >= strlen($domain) && substr($dothost, -strlen($domain)) === $domain) 150 | { 151 | foreach ($paths as $path => $cookies2) 152 | { 153 | if (substr($cookiepath, 0, strlen($path)) == $path) 154 | { 155 | foreach ($cookies2 as $num => $info) 156 | { 157 | if (isset($info["expires_ts"]) && $this->GetExpiresTimestamp($info["expires_ts"]) < time()) unset($this->data["cookies"][$domain][$path][$num]); 158 | else if ($secure || !isset($info["secure"])) $cookies[$info["name"]] = $info["value"]; 159 | } 160 | 161 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 162 | } 163 | } 164 | 165 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 166 | } 167 | } 168 | 169 | $cookies2 = array(); 170 | foreach ($cookies as $name => $value) $cookies2[] = rawurlencode($name) . "=" . rawurlencode($value); 171 | $headers["Cookie"] = implode("; ", $cookies2); 172 | if ($headers["Cookie"] == "") unset($headers["Cookie"]); 173 | 174 | // Generate the final options array. 175 | $state["options"] = array_merge($state["httpopts"], $state["tempoptions"]); 176 | $state["options"]["headers"] = $headers; 177 | if ($state["timeout"] !== false) $state["options"]["timeout"] = HTTP::GetTimeLeft($state["startts"], $state["timeout"]); 178 | 179 | // Let a callback handle any additional state changes. 180 | if (isset($state["options"]["pre_retrievewebpage_callback"]) && is_callable($state["options"]["pre_retrievewebpage_callback"]) && !call_user_func_array($state["options"]["pre_retrievewebpage_callback"], array(&$state))) 181 | { 182 | return array("success" => false, "error" => self::WBTranslate("Pre-RetrieveWebpage callback returned with a failure condition for '%s'.", $state["url"]), "errorcode" => "pre_retrievewebpage_callback"); 183 | } 184 | 185 | // Process the request. 186 | $result = HTTP::RetrieveWebpage($state["url"], $state["options"]); 187 | $result["url"] = $state["url"]; 188 | unset($state["options"]["files"]); 189 | unset($state["options"]["body"]); 190 | unset($state["tempoptions"]["headers"]["Content-Type"]); 191 | $result["options"] = $state["options"]; 192 | $result["firstreqts"] = $state["startts"]; 193 | $result["numredirects"] = $state["numredirects"]; 194 | $result["redirectts"] = $state["redirectts"]; 195 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 196 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 197 | if (!$result["success"]) return array("success" => false, "error" => self::WBTranslate("Unable to retrieve content. %s", $result["error"]), "info" => $result, "state" => $state, "errorcode" => "retrievewebpage"); 198 | 199 | if (isset($state["options"]["async"]) && $state["options"]["async"]) 200 | { 201 | $state["async"] = true; 202 | $state["httpstate"] = $result["state"]; 203 | 204 | $state["state"] = "process_async"; 205 | } 206 | else 207 | { 208 | $state["result"] = $result; 209 | 210 | $state["state"] = "post_retrieval"; 211 | } 212 | 213 | break; 214 | } 215 | case "process_async": 216 | { 217 | // Run a cycle of the HTTP state processor. 218 | $result = HTTP::ProcessState($state["httpstate"]); 219 | if (!$result["success"]) return $result; 220 | 221 | $result["url"] = $state["url"]; 222 | $result["options"] = $state["options"]; 223 | unset($result["options"]["files"]); 224 | unset($result["options"]["body"]); 225 | $result["firstreqts"] = $state["startts"]; 226 | $result["numredirects"] = $state["numredirects"]; 227 | $result["redirectts"] = $state["redirectts"]; 228 | if (isset($result["rawsendsize"])) $state["totalrawsendsize"] += $result["rawsendsize"]; 229 | $result["totalrawsendsize"] = $state["totalrawsendsize"]; 230 | 231 | $state["httpstate"] = false; 232 | $state["result"] = $result; 233 | 234 | $state["state"] = "post_retrieval"; 235 | 236 | break; 237 | } 238 | case "post_retrieval": 239 | { 240 | // Set up structures for another round. 241 | if ($this->data["autoreferer"]) $this->data["referer"] = $state["url"]; 242 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"]) 243 | { 244 | $state["redirectts"] = microtime(true); 245 | 246 | unset($state["tempoptions"]["method"]); 247 | unset($state["tempoptions"]["write_body_callback"]); 248 | unset($state["tempoptions"]["body"]); 249 | unset($state["tempoptions"]["postvars"]); 250 | unset($state["tempoptions"]["files"]); 251 | 252 | $state["tempoptions"]["headers"]["Referer"] = $state["url"]; 253 | $state["url"] = $state["result"]["headers"]["Location"][0]; 254 | 255 | // Generate an absolute URL. 256 | if ($this->data["referer"] != "") $state["url"] = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $state["url"]); 257 | 258 | $urlinfo2 = HTTP::ExtractURL($state["url"]); 259 | 260 | if (!isset($this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) || !$this->data["allowedredirprotocols"][$urlinfo2["scheme"]]) 261 | { 262 | return array("success" => false, "error" => self::WBTranslate("Protocol '%s' is not allowed. Server attempted to redirect to '%s'.", $urlinfo2["scheme"], $state["url"]), "info" => $state["result"], "errorcode" => "allowed_redir_protocols"); 263 | } 264 | 265 | if ($urlinfo2["host"] != $state["urlinfo"]["host"]) 266 | { 267 | unset($state["tempoptions"]["headers"]["Host"]); 268 | unset($state["httpopts"]["headers"]["Host"]); 269 | 270 | unset($state["httpopts"]["headers"]["Authorization"]); 271 | unset($state["tempoptions"]["headers"]["Authorization"]); 272 | } 273 | 274 | $state["urlinfo"] = $urlinfo2; 275 | $state["numredirects"]++; 276 | } 277 | 278 | // Handle any 'Set-Cookie' headers. 279 | if (isset($state["result"]["headers"]["Set-Cookie"])) 280 | { 281 | foreach ($state["result"]["headers"]["Set-Cookie"] as $cookie) 282 | { 283 | $items = explode(";", $cookie); 284 | $item = trim(array_shift($items)); 285 | if ($item != "") 286 | { 287 | $cookie2 = array(); 288 | $pos = strpos($item, "="); 289 | if ($pos === false) 290 | { 291 | $cookie2["name"] = urldecode($item); 292 | $cookie2["value"] = ""; 293 | } 294 | else 295 | { 296 | $cookie2["name"] = urldecode(substr($item, 0, $pos)); 297 | $cookie2["value"] = urldecode(substr($item, $pos + 1)); 298 | } 299 | 300 | $cookie = array(); 301 | foreach ($items as $item) 302 | { 303 | $item = trim($item); 304 | if ($item != "") 305 | { 306 | $pos = strpos($item, "="); 307 | if ($pos === false) $cookie[strtolower(trim(urldecode($item)))] = ""; 308 | else $cookie[strtolower(trim(urldecode(substr($item, 0, $pos))))] = urldecode(substr($item, $pos + 1)); 309 | } 310 | } 311 | $cookie = array_merge($cookie, $cookie2); 312 | 313 | if (isset($cookie["expires"])) 314 | { 315 | $ts = HTTP::GetDateTimestamp($cookie["expires"]); 316 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", ($ts === false ? time() - 24 * 60 * 60 : $ts)); 317 | } 318 | else if (isset($cookie["max-age"])) 319 | { 320 | $cookie["expires_ts"] = gmdate("Y-m-d H:i:s", time() + (int)$cookie["max-age"]); 321 | } 322 | else 323 | { 324 | unset($cookie["expires_ts"]); 325 | } 326 | 327 | if (!isset($cookie["domain"])) $cookie["domain"] = $state["dothost"]; 328 | if (!isset($cookie["path"])) $cookie["path"] = $state["cookiepath"]; 329 | 330 | $this->SetCookie($cookie); 331 | } 332 | } 333 | } 334 | 335 | if ($state["numfollow"] > 0) $state["numfollow"]--; 336 | 337 | // If this is a redirect, handle it by starting over. 338 | if (isset($state["result"]["headers"]["Location"]) && $this->data["followlocation"] && $state["numfollow"]) 339 | { 340 | $state["result"] = false; 341 | 342 | $state["state"] = "initialize"; 343 | } 344 | else 345 | { 346 | $state["result"]["numredirects"] = $state["numredirects"]; 347 | $state["result"]["redirectts"] = $state["redirectts"]; 348 | 349 | // Extract the forms from the page in a parsed format. 350 | // Call WebBrowser::GenerateFormRequest() to prepare an actual request for Process(). 351 | if ($this->data["extractforms"]) $state["result"]["forms"] = $this->ExtractForms($state["result"]["url"], $state["result"]["body"], (isset($state["tempoptions"]["extractforms_hint"]) ? $state["tempoptions"]["extractforms_hint"] : false)); 352 | 353 | $state["state"] = "done"; 354 | } 355 | 356 | break; 357 | } 358 | } 359 | } 360 | 361 | return $state["result"]; 362 | } 363 | 364 | public function Process($url, $tempoptions = array()) 365 | { 366 | $startts = microtime(true); 367 | $redirectts = $startts; 368 | 369 | // Handle older function call: Process($url, $profile, $tempoptions) 370 | if (is_string($tempoptions)) 371 | { 372 | $args = func_get_args(); 373 | if (count($args) < 3) $tempoptions = array(); 374 | else $tempoptions = $args[2]; 375 | 376 | $tempoptions["profile"] = $args[1]; 377 | } 378 | 379 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 380 | 381 | if (isset($tempoptions["timeout"])) $timeout = $tempoptions["timeout"]; 382 | else if (isset($this->data["httpopts"]["timeout"])) $timeout = $this->data["httpopts"]["timeout"]; 383 | else $timeout = false; 384 | 385 | // Deal with possible application hanging issues. 386 | if (isset($tempoptions["streamtimeout"])) $streamtimeout = $tempoptions["streamtimeout"]; 387 | else if (isset($this->data["httpopts"]["streamtimeout"])) $streamtimeout = $this->data["httpopts"]["streamtimeout"]; 388 | else $streamtimeout = 300; 389 | $tempoptions["streamtimeout"] = $streamtimeout; 390 | 391 | if (!isset($this->data["httpopts"]["headers"])) $this->data["httpopts"]["headers"] = array(); 392 | $this->data["httpopts"]["headers"] = HTTP::NormalizeHeaders($this->data["httpopts"]["headers"]); 393 | unset($this->data["httpopts"]["method"]); 394 | unset($this->data["httpopts"]["write_body_callback"]); 395 | unset($this->data["httpopts"]["body"]); 396 | unset($this->data["httpopts"]["postvars"]); 397 | unset($this->data["httpopts"]["files"]); 398 | 399 | $httpopts = $this->data["httpopts"]; 400 | $numfollow = $this->data["maxfollow"]; 401 | $numredirects = 0; 402 | $totalrawsendsize = 0; 403 | 404 | if (!isset($tempoptions["headers"])) $tempoptions["headers"] = array(); 405 | $tempoptions["headers"] = HTTP::NormalizeHeaders($tempoptions["headers"]); 406 | if (isset($tempoptions["headers"]["Referer"])) $this->data["referer"] = $tempoptions["headers"]["Referer"]; 407 | 408 | // If a referrer is specified, use it to generate an absolute URL. 409 | if ($this->data["referer"] != "") $url = HTTP::ConvertRelativeToAbsoluteURL($this->data["referer"], $url); 410 | 411 | $urlinfo = HTTP::ExtractURL($url); 412 | 413 | // Initialize the process state array. 414 | $state = array( 415 | "async" => false, 416 | "startts" => $startts, 417 | "redirectts" => $redirectts, 418 | "timeout" => $timeout, 419 | "tempoptions" => $tempoptions, 420 | "httpopts" => $httpopts, 421 | "numfollow" => $numfollow, 422 | "numredirects" => $numredirects, 423 | "totalrawsendsize" => $totalrawsendsize, 424 | "profile" => $profile, 425 | "url" => $url, 426 | "urlinfo" => $urlinfo, 427 | 428 | "state" => "initialize", 429 | "httpstate" => false, 430 | "result" => false, 431 | ); 432 | 433 | // Run at least one state cycle to properly initialize the state array. 434 | $result = $this->ProcessState($state); 435 | 436 | // Return the state for async calls. Caller must call ProcessState(). 437 | if ($state["async"]) return array("success" => true, "state" => $state); 438 | 439 | return $result; 440 | } 441 | 442 | // Implements the correct MultiAsyncHelper responses for WebBrowser instances. 443 | public function ProcessAsync__Handler($mode, &$data, $key, &$info) 444 | { 445 | switch ($mode) 446 | { 447 | case "init": 448 | { 449 | if ($info["init"]) $data = $info["keep"]; 450 | else 451 | { 452 | $info["result"] = $this->Process($info["url"], $info["tempoptions"]); 453 | if (!$info["result"]["success"]) 454 | { 455 | $info["keep"] = false; 456 | 457 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 458 | } 459 | else 460 | { 461 | $info["state"] = $info["result"]["state"]; 462 | 463 | // Move to the live queue. 464 | $data = true; 465 | } 466 | } 467 | 468 | break; 469 | } 470 | case "update": 471 | case "read": 472 | case "write": 473 | { 474 | if ($info["keep"]) 475 | { 476 | $info["result"] = $this->ProcessState($info["state"]); 477 | if ($info["result"]["success"] || $info["result"]["errorcode"] !== "no_data") $info["keep"] = false; 478 | 479 | if (is_callable($info["callback"])) call_user_func_array($info["callback"], array($key, $info["url"], $info["result"])); 480 | 481 | if ($mode === "update") $data = $info["keep"]; 482 | } 483 | 484 | break; 485 | } 486 | case "readfps": 487 | { 488 | if ($info["state"]["httpstate"] !== false && HTTP::WantRead($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 489 | 490 | break; 491 | } 492 | case "writefps": 493 | { 494 | if ($info["state"]["httpstate"] !== false && HTTP::WantWrite($info["state"]["httpstate"])) $data[$key] = $info["state"]["httpstate"]["fp"]; 495 | 496 | break; 497 | } 498 | case "cleanup": 499 | { 500 | // When true, caller is removing. Otherwise, detaching from the queue. 501 | if ($data === true) 502 | { 503 | if (isset($info["state"])) 504 | { 505 | if ($info["state"]["httpstate"] !== false) HTTP::ForceClose($info["state"]["httpstate"]); 506 | 507 | unset($info["state"]); 508 | } 509 | 510 | $info["keep"] = false; 511 | } 512 | 513 | break; 514 | } 515 | } 516 | } 517 | 518 | public function ProcessAsync($helper, $key, $callback, $url, $tempoptions = array()) 519 | { 520 | $tempoptions["async"] = true; 521 | 522 | // Handle older function call: ProcessAsync($helper, $key, $callback, $url, $profile, $tempoptions) 523 | if (is_string($tempoptions)) 524 | { 525 | $args = func_get_args(); 526 | if (count($args) < 6) $tempoptions = array(); 527 | else $tempoptions = $args[5]; 528 | 529 | $tempoptions["profile"] = $args[4]; 530 | } 531 | 532 | $profile = (isset($tempoptions["profile"]) ? $tempoptions["profile"] : "auto"); 533 | 534 | $info = array( 535 | "init" => false, 536 | "keep" => true, 537 | "callback" => $callback, 538 | "url" => $url, 539 | "tempoptions" => $tempoptions, 540 | "result" => false 541 | ); 542 | 543 | $helper->Set($key, $info, array($this, "ProcessAsync__Handler")); 544 | 545 | return array("success" => true); 546 | } 547 | 548 | public function ExtractForms($baseurl, $data, $hint = false) 549 | { 550 | $result = array(); 551 | 552 | $lasthint = ""; 553 | $hintmap = array(); 554 | if ($this->html === false) 555 | { 556 | if (!class_exists("simple_html_dom", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/simple_html_dom.php"; 557 | 558 | $this->html = new simple_html_dom(); 559 | } 560 | $this->html->load($data); 561 | $rows = $this->html->find("label[for]"); 562 | foreach ($rows as $row) 563 | { 564 | $hintmap[trim($row->for)] = trim($row->plaintext); 565 | } 566 | $html5rows = $this->html->find("input[form],textarea[form],select[form],button[form],datalist[id]" . ($hint !== false ? "," . $hint : "")); 567 | $rows = $this->html->find("form"); 568 | foreach ($rows as $row) 569 | { 570 | $info = array(); 571 | if (isset($row->id)) $info["id"] = trim($row->id); 572 | if (isset($row->name)) $info["name"] = (string)$row->name; 573 | $info["action"] = (isset($row->action) ? HTTP::ConvertRelativeToAbsoluteURL($baseurl, (string)$row->action) : $baseurl); 574 | $info["method"] = (isset($row->method) && strtolower(trim($row->method)) == "post" ? "post" : "get"); 575 | if ($info["method"] == "post") $info["enctype"] = (isset($row->enctype) ? strtolower($row->enctype) : "application/x-www-form-urlencoded"); 576 | if (isset($row->{"accept-charset"})) $info["accept-charset"] = (string)$row->{"accept-charset"}; 577 | 578 | $fields = array(); 579 | $rows2 = $row->find("input,textarea,select,button" . ($hint !== false ? "," . $hint : "")); 580 | foreach ($rows2 as $row2) 581 | { 582 | if (!isset($row2->form)) 583 | { 584 | if (isset($row2->id) && $row2->id != "" && isset($hintmap[trim($row2->id)])) $lasthint = $hintmap[trim($row2->id)]; 585 | 586 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 587 | } 588 | } 589 | 590 | // Handle HTML5. 591 | if (isset($info["id"]) && $info["id"] != "") 592 | { 593 | foreach ($html5rows as $row2) 594 | { 595 | if (strpos(" " . $info["id"] . " ", " " . $row2->form . " ") !== false) 596 | { 597 | if (isset($hintmap[$info["id"]])) $lasthint = $hintmap[$info["id"]]; 598 | 599 | $this->ExtractFieldFromDOM($fields, $row2, $lasthint); 600 | } 601 | } 602 | } 603 | 604 | $form = new WebBrowserForm(); 605 | $form->info = $info; 606 | $form->fields = $fields; 607 | $result[] = $form; 608 | } 609 | 610 | return $result; 611 | } 612 | 613 | private function ExtractFieldFromDOM(&$fields, $row, &$lasthint) 614 | { 615 | switch ($row->tag) 616 | { 617 | case "input": 618 | { 619 | if (!isset($row->name) && ($row->type === "submit" || $row->type === "image")) $row->name = ""; 620 | 621 | if (isset($row->name) && is_string($row->name)) 622 | { 623 | $field = array( 624 | "id" => (isset($row->id) ? (string)$row->id : false), 625 | "type" => "input." . (isset($row->type) ? strtolower($row->type) : "text"), 626 | "name" => $row->name, 627 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 628 | ); 629 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") 630 | { 631 | $field["checked"] = (isset($row->checked)); 632 | 633 | if ($field["value"] === "") $field["value"] = "on"; 634 | } 635 | 636 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 637 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image") $field["hint"] = $field["type"] . "|" . $field["value"]; 638 | else if ($lasthint !== "") $field["hint"] = $lasthint; 639 | 640 | $fields[] = $field; 641 | 642 | $lasthint = ""; 643 | } 644 | 645 | break; 646 | } 647 | case "textarea": 648 | { 649 | if (isset($row->name) && is_string($row->name)) 650 | { 651 | $field = array( 652 | "id" => (isset($row->id) ? (string)$row->id : false), 653 | "type" => "textarea", 654 | "name" => $row->name, 655 | "value" => html_entity_decode($row->innertext, ENT_COMPAT, "UTF-8") 656 | ); 657 | 658 | if (isset($row->placeholder)) $field["hint"] = trim($row->placeholder); 659 | else if ($lasthint !== "") $field["hint"] = $lasthint; 660 | 661 | $fields[] = $field; 662 | 663 | $lasthint = ""; 664 | } 665 | 666 | break; 667 | } 668 | case "select": 669 | { 670 | if (isset($row->name) && is_string($row->name)) 671 | { 672 | if (isset($row->multiple)) 673 | { 674 | // Change the type into multiple checkboxes. 675 | $rows = $row->find("option"); 676 | foreach ($rows as $row2) 677 | { 678 | $field = array( 679 | "id" => (isset($row->id) ? (string)$row->id : false), 680 | "type" => "input.checkbox", 681 | "name" => $row->name, 682 | "value" => (isset($row2->value) ? html_entity_decode($row2->value, ENT_COMPAT, "UTF-8") : ""), 683 | "display" => (string)$row2->innertext 684 | ); 685 | if ($lasthint !== "") $field["hint"] = $lasthint; 686 | 687 | $fields[] = $field; 688 | } 689 | } 690 | else 691 | { 692 | $val = false; 693 | $options = array(); 694 | $rows = $row->find("option"); 695 | foreach ($rows as $row2) 696 | { 697 | $options[$row2->value] = (string)$row2->innertext; 698 | 699 | if ($val === false && isset($row2->selected)) $val = html_entity_decode($row2->value, ENT_COMPAT, "UTF-8"); 700 | } 701 | if ($val === false && count($options)) 702 | { 703 | $val = array_keys($options); 704 | $val = $val[0]; 705 | } 706 | if ($val === false) $val = ""; 707 | 708 | $field = array( 709 | "id" => (isset($row->id) ? (string)$row->id : false), 710 | "type" => "select", 711 | "name" => $row->name, 712 | "value" => $val, 713 | "options" => $options 714 | ); 715 | if ($lasthint !== "") $field["hint"] = $lasthint; 716 | 717 | $fields[] = $field; 718 | } 719 | 720 | $lasthint = ""; 721 | } 722 | 723 | break; 724 | } 725 | case "button": 726 | { 727 | if (isset($row->name) && is_string($row->name)) 728 | { 729 | $field = array( 730 | "id" => (isset($row->id) ? (string)$row->id : false), 731 | "type" => "button." . (isset($row->type) ? strtolower($row->type) : "submit"), 732 | "name" => $row->name, 733 | "value" => (isset($row->value) ? html_entity_decode($row->value, ENT_COMPAT, "UTF-8") : "") 734 | ); 735 | $field["hint"] = (trim($row->plaintext) !== "" ? trim($row->plaintext) : "button|" . $field["value"]); 736 | 737 | $fields[] = $field; 738 | 739 | $lasthint = ""; 740 | } 741 | 742 | break; 743 | } 744 | case "datalist": 745 | { 746 | // Do nothing since browsers don't actually enforce this tag's values. 747 | 748 | break; 749 | } 750 | default: 751 | { 752 | // Hint for the next element. 753 | $lasthint = (string)$row->plaintext; 754 | 755 | break; 756 | } 757 | } 758 | } 759 | 760 | public static function InteractiveFormFill($forms, $showselected = false) 761 | { 762 | if (!is_array($forms)) $forms = array($forms); 763 | 764 | if (!count($forms)) return false; 765 | 766 | if (count($forms) == 1) $form = reset($forms); 767 | else 768 | { 769 | echo self::WBTranslate("There are multiple forms available to fill out:\n"); 770 | foreach ($forms as $num => $form) 771 | { 772 | echo self::WBTranslate("\t%d:\n", $num + 1); 773 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t\t%s: %s\n", $key, $val); 774 | echo self::WBTranslate("\t\tfields: %d\n", count($form->GetVisibleFields(false))); 775 | echo self::WBTranslate("\t\tbuttons: %d\n", count($form->GetVisibleFields(true)) - count($form->GetVisibleFields(false))); 776 | echo "\n"; 777 | } 778 | 779 | do 780 | { 781 | echo self::WBTranslate("Select: "); 782 | 783 | $num = (int)trim(fgets(STDIN)) - 1; 784 | } while (!isset($forms[$num])); 785 | 786 | $form = $forms[$num]; 787 | } 788 | 789 | if ($showselected) 790 | { 791 | echo self::WBTranslate("Selected form:\n"); 792 | foreach ($form->info as $key => $val) echo self::WBTranslate("\t%s: %s\n", $key, $val); 793 | echo "\n"; 794 | } 795 | 796 | if (count($form->GetVisibleFields(false))) 797 | { 798 | echo self::WBTranslate("Select form fields by field number to edit a field. When ready to submit the form, leave 'Field number' empty.\n\n"); 799 | 800 | do 801 | { 802 | echo self::WBTranslate("Editable form fields:\n"); 803 | foreach ($form->fields as $num => $field) 804 | { 805 | if ($field["type"] == "input.hidden" || $field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button.") continue; 806 | 807 | echo self::WBTranslate("\t%d: %s - %s\n", $num + 1, $field["name"], (is_array($field["value"]) ? json_encode($field["value"], JSON_PRETTY_PRINT) : $field["value"]) . (($field["type"] == "input.radio" || $field["type"] == "input.checkbox") ? ($field["checked"] ? self::WBTranslate(" [Y]") : self::WBTranslate(" [N]")) : "") . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 808 | } 809 | echo "\n"; 810 | 811 | do 812 | { 813 | echo self::WBTranslate("Field number: "); 814 | 815 | $num = trim(fgets(STDIN)); 816 | if ($num === "") break; 817 | 818 | $num = (int)$num - 1; 819 | } while (!isset($form->fields[$num]) || $form->fields[$num]["type"] == "input.hidden" || $form->fields[$num]["type"] == "input.submit" || $form->fields[$num]["type"] == "input.image" || $form->fields[$num]["type"] == "input.button" || substr($form->fields[$num]["type"], 0, 7) == "button."); 820 | 821 | if ($num === "") 822 | { 823 | echo "\n"; 824 | 825 | break; 826 | } 827 | 828 | $field = $form->fields[$num]; 829 | $prefix = (isset($field["hint"]) && $field["hint"] !== "" ? $field["hint"] . " | " : "") . $field["name"]; 830 | 831 | if ($field["type"] == "select") 832 | { 833 | echo self::WBTranslate("[%s] Options:\n", $prefix); 834 | foreach ($field["options"] as $key => $val) 835 | { 836 | echo self::WBTranslate("\t%s: %s\n"); 837 | } 838 | 839 | do 840 | { 841 | echo self::WBTranslate("[%s] Select: ", $prefix); 842 | 843 | $select = rtrim(fgets(STDIN)); 844 | } while (!isset($field["options"][$select])); 845 | 846 | $form->fields[$num]["value"] = $select; 847 | } 848 | else if ($field["type"] == "input.radio") 849 | { 850 | $form->SetFormValue($field["name"], $field["value"], true, "input.radio"); 851 | } 852 | else if ($field["type"] == "input.checkbox") 853 | { 854 | $form->fields[$num]["checked"] = !$field["checked"]; 855 | } 856 | else if ($field["type"] == "input.file") 857 | { 858 | do 859 | { 860 | echo self::WBTranslate("[%s] Filename: ", $prefix); 861 | 862 | $filename = rtrim(fgets(STDIN)); 863 | } while ($filename !== "" && !file_exists($filename)); 864 | 865 | if ($filename === "") $form->fields[$num]["value"] = ""; 866 | else 867 | { 868 | $form->fields[$num]["value"] = array( 869 | "filename" => $filename, 870 | "type" => "application/octet-stream", 871 | "datafile" => $filename 872 | ); 873 | } 874 | } 875 | else 876 | { 877 | echo self::WBTranslate("[%s] New value: ", $prefix); 878 | 879 | $form->fields[$num]["value"] = rtrim(fgets(STDIN)); 880 | } 881 | 882 | echo "\n"; 883 | 884 | } while (1); 885 | } 886 | 887 | $submitoptions = array(array("name" => self::WBTranslate("Default action"), "value" => self::WBTranslate("Might not work"), "hint" => "Default action")); 888 | foreach ($form->fields as $num => $field) 889 | { 890 | if ($field["type"] != "input.submit" && $field["type"] != "input.image" && $field["type"] != "input.button" && $field["type"] != "button.submit") continue; 891 | 892 | $submitoptions[] = $field; 893 | } 894 | 895 | if (count($submitoptions) <= 2) $num = count($submitoptions) - 1; 896 | else 897 | { 898 | echo self::WBTranslate("Available submit buttons:\n"); 899 | foreach ($submitoptions as $num => $field) 900 | { 901 | echo self::WBTranslate("\t%d: %s - %s\n", $num, $field["name"], $field["value"] . (isset($field["hint"]) && $field["hint"] !== "" ? " [" . $field["hint"] . "]" : "")); 902 | } 903 | echo "\n"; 904 | 905 | do 906 | { 907 | echo self::WBTranslate("Select: "); 908 | 909 | $num = (int)fgets(STDIN); 910 | } while (!isset($submitoptions[$num])); 911 | 912 | echo "\n"; 913 | } 914 | 915 | $result = $form->GenerateFormRequest(($num ? $submitoptions[$num]["name"] : false), ($num ? $submitoptions[$num]["value"] : false)); 916 | 917 | return $result; 918 | } 919 | 920 | public function GetCookies() 921 | { 922 | return $this->data["cookies"]; 923 | } 924 | 925 | public function SetCookie($cookie) 926 | { 927 | if (!isset($cookie["domain"]) || !isset($cookie["path"]) || !isset($cookie["name"]) || !isset($cookie["value"])) return array("success" => false, "error" => self::WBTranslate("SetCookie() requires 'domain', 'path', 'name', and 'value' to be options."), "errorcode" => "missing_information"); 928 | 929 | $cookie["domain"] = strtolower($cookie["domain"]); 930 | if (substr($cookie["domain"], 0, 1) != ".") $cookie["domain"] = "." . $cookie["domain"]; 931 | 932 | $cookie["path"] = str_replace("\\", "/", $cookie["path"]); 933 | if (substr($cookie["path"], -1) != "/") $cookie["path"] = "/"; 934 | 935 | if (!isset($this->data["cookies"][$cookie["domain"]])) $this->data["cookies"][$cookie["domain"]] = array(); 936 | if (!isset($this->data["cookies"][$cookie["domain"]][$cookie["path"]])) $this->data["cookies"][$cookie["domain"]][$cookie["path"]] = array(); 937 | $this->data["cookies"][$cookie["domain"]][$cookie["path"]][$cookie["name"]] = $cookie; 938 | 939 | return array("success" => true); 940 | } 941 | 942 | // Simulates closing a web browser. 943 | public function DeleteSessionCookies() 944 | { 945 | foreach ($this->data["cookies"] as $domain => $paths) 946 | { 947 | foreach ($paths as $path => $cookies) 948 | { 949 | foreach ($cookies as $num => $info) 950 | { 951 | if (!isset($info["expires_ts"])) unset($this->data["cookies"][$domain][$path][$num]); 952 | } 953 | 954 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 955 | } 956 | 957 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 958 | } 959 | } 960 | 961 | public function DeleteCookies($domainpattern, $pathpattern, $namepattern) 962 | { 963 | foreach ($this->data["cookies"] as $domain => $paths) 964 | { 965 | if ($domainpattern == "" || substr($domain, -strlen($domainpattern)) == $domainpattern) 966 | { 967 | foreach ($paths as $path => $cookies) 968 | { 969 | if ($pathpattern == "" || substr($path, 0, strlen($pathpattern)) == $pathpattern) 970 | { 971 | foreach ($cookies as $num => $info) 972 | { 973 | if ($namepattern == "" || strpos($info["name"], $namepattern) !== false) unset($this->data["cookies"][$domain][$path][$num]); 974 | } 975 | 976 | if (!count($this->data["cookies"][$domain][$path])) unset($this->data["cookies"][$domain][$path]); 977 | } 978 | } 979 | 980 | if (!count($this->data["cookies"][$domain])) unset($this->data["cookies"][$domain]); 981 | } 982 | } 983 | } 984 | 985 | private function GetExpiresTimestamp($ts) 986 | { 987 | $year = (int)substr($ts, 0, 4); 988 | $month = (int)substr($ts, 5, 2); 989 | $day = (int)substr($ts, 8, 2); 990 | $hour = (int)substr($ts, 11, 2); 991 | $min = (int)substr($ts, 14, 2); 992 | $sec = (int)substr($ts, 17, 2); 993 | 994 | return gmmktime($hour, $min, $sec, $month, $day, $year); 995 | } 996 | 997 | public static function WBTranslate() 998 | { 999 | $args = func_get_args(); 1000 | if (!count($args)) return ""; 1001 | 1002 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 1003 | } 1004 | } 1005 | 1006 | class WebBrowserForm 1007 | { 1008 | public $info, $fields; 1009 | 1010 | public function __construct() 1011 | { 1012 | $this->info = array(); 1013 | $this->fields = array(); 1014 | } 1015 | 1016 | public function FindFormFields($name = false, $value = false, $type = false) 1017 | { 1018 | $fields = array(); 1019 | foreach ($this->fields as $num => $field) 1020 | { 1021 | if (($type === false || $field["type"] === $type) && ($name === false || $field["name"] === $name) && ($value === false || $field["value"] === $value)) 1022 | { 1023 | $fields[] = $field; 1024 | } 1025 | } 1026 | 1027 | return $fields; 1028 | } 1029 | 1030 | public function GetHintMap() 1031 | { 1032 | $result = array(); 1033 | foreach ($this->fields as $num => $field) 1034 | { 1035 | if (isset($field["hint"])) $result[$field["hint"]] = $field["name"]; 1036 | } 1037 | 1038 | return $result; 1039 | } 1040 | 1041 | public function GetVisibleFields($submit) 1042 | { 1043 | $result = array(); 1044 | foreach ($this->fields as $num => $field) 1045 | { 1046 | if ($field["type"] == "input.hidden" || (!$submit && ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "input.button" || substr($field["type"], 0, 7) == "button."))) continue; 1047 | 1048 | $result[$num] = $field; 1049 | } 1050 | 1051 | return $result; 1052 | } 1053 | 1054 | public function GetFormValue($name, $checkval = false, $type = false) 1055 | { 1056 | $val = false; 1057 | foreach ($this->fields as $field) 1058 | { 1059 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1060 | { 1061 | if (is_string($checkval)) 1062 | { 1063 | if ($checkval === $field["value"]) 1064 | { 1065 | if ($field["type"] == "input.radio" || $field["type"] == "input.checkbox") $val = $field["checked"]; 1066 | else $val = $field["value"]; 1067 | } 1068 | } 1069 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1070 | { 1071 | $val = $field["value"]; 1072 | } 1073 | } 1074 | } 1075 | 1076 | return $val; 1077 | } 1078 | 1079 | public function SetFormValue($name, $value, $checked = false, $type = false, $create = false) 1080 | { 1081 | $result = false; 1082 | foreach ($this->fields as $num => $field) 1083 | { 1084 | if (($type === false || $field["type"] === $type) && $field["name"] === $name) 1085 | { 1086 | if ($field["type"] == "input.radio") 1087 | { 1088 | $this->fields[$num]["checked"] = ($field["value"] === $value ? $checked : false); 1089 | $result = true; 1090 | } 1091 | else if ($field["type"] == "input.checkbox") 1092 | { 1093 | if ($field["value"] === $value) $this->fields[$num]["checked"] = $checked; 1094 | $result = true; 1095 | } 1096 | else if ($field["type"] != "select" || !isset($field["options"]) || isset($field["options"][$value])) 1097 | { 1098 | $this->fields[$num]["value"] = $value; 1099 | $result = true; 1100 | } 1101 | } 1102 | } 1103 | 1104 | // Add the field if it doesn't exist. 1105 | if (!$result && $create) 1106 | { 1107 | $this->fields[] = array( 1108 | "id" => false, 1109 | "type" => ($type !== false ? $type : "input.text"), 1110 | "name" => $name, 1111 | "value" => $value, 1112 | "checked" => $checked 1113 | ); 1114 | } 1115 | 1116 | return $result; 1117 | } 1118 | 1119 | public function GenerateFormRequest($submitname = false, $submitvalue = false) 1120 | { 1121 | $method = $this->info["method"]; 1122 | $fields = array(); 1123 | $files = array(); 1124 | foreach ($this->fields as $field) 1125 | { 1126 | if ($field["type"] == "input.file") 1127 | { 1128 | if (is_array($field["value"])) 1129 | { 1130 | $field["value"]["name"] = $field["name"]; 1131 | $files[] = $field["value"]; 1132 | $method = "post"; 1133 | } 1134 | } 1135 | else if ($field["type"] == "input.reset" || $field["type"] == "button.reset") 1136 | { 1137 | } 1138 | else if ($field["type"] == "input.submit" || $field["type"] == "input.image" || $field["type"] == "button.submit") 1139 | { 1140 | if (($submitname === false || $field["name"] === $submitname) && ($submitvalue === false || $field["value"] === $submitvalue)) 1141 | { 1142 | if ($submitname !== "") 1143 | { 1144 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1145 | $fields[$field["name"]][] = $field["value"]; 1146 | } 1147 | 1148 | if ($field["type"] == "input.image") 1149 | { 1150 | if (!isset($fields["x"])) $fields["x"] = array(); 1151 | $fields["x"][] = "1"; 1152 | 1153 | if (!isset($fields["y"])) $fields["y"] = array(); 1154 | $fields["y"][] = "1"; 1155 | } 1156 | } 1157 | } 1158 | else if (($field["type"] != "input.radio" && $field["type"] != "input.checkbox") || $field["checked"]) 1159 | { 1160 | if (!isset($fields[$field["name"]])) $fields[$field["name"]] = array(); 1161 | $fields[$field["name"]][] = $field["value"]; 1162 | } 1163 | } 1164 | 1165 | if ($method == "get") 1166 | { 1167 | $url = HTTP::ExtractURL($this->info["action"]); 1168 | unset($url["query"]); 1169 | $url["queryvars"] = $fields; 1170 | $result = array( 1171 | "url" => HTTP::CondenseURL($url), 1172 | "options" => array() 1173 | ); 1174 | } 1175 | else 1176 | { 1177 | $result = array( 1178 | "url" => $this->info["action"], 1179 | "options" => array( 1180 | "postvars" => $fields, 1181 | "files" => $files 1182 | ) 1183 | ); 1184 | } 1185 | 1186 | return $result; 1187 | } 1188 | } 1189 | ?> -------------------------------------------------------------------------------- /php/support/sdk_barebones_cms_api.php: -------------------------------------------------------------------------------- 1 | web = new WebBrowser(); 15 | $this->fp = false; 16 | $this->debug = false; 17 | $this->host = false; 18 | $this->apikey = false; 19 | $this->apisecret = false; 20 | } 21 | 22 | public function SetAccessInfo($host, $apikey, $apisecret = false) 23 | { 24 | $this->web = new WebBrowser(); 25 | if (is_resource($this->fp)) @fclose($this->fp); 26 | $this->fp = false; 27 | $this->host = $host; 28 | $this->apikey = $apikey; 29 | $this->apisecret = $apisecret; 30 | } 31 | 32 | public function CheckAccess() 33 | { 34 | $apipath = array( 35 | "ver" => "1", 36 | "ts" => time(), 37 | "api" => "access" 38 | ); 39 | 40 | return $this->RunAPI("GET", $apipath); 41 | } 42 | 43 | public function Internal_BulkCallback($response, $body, &$opts) 44 | { 45 | if ($response["code"] == 200) 46 | { 47 | $opts["body"] .= $body; 48 | 49 | while (($pos = strpos($opts["body"], "\n")) !== false) 50 | { 51 | $data = @json_decode(trim(substr($opts["body"], 0, $pos)), true); 52 | $opts["body"] = substr($opts["body"], $pos + 1); 53 | 54 | if (is_array($data) && isset($data["success"])) call_user_func_array($opts["callback"], array($data)); 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | public function GetRevisions($id, $revision = false, $bulkcallback = false, $limit = false, $offset = false) 62 | { 63 | $apipath = array( 64 | "ver" => "1", 65 | "ts" => time(), 66 | "api" => "revisions", 67 | "id" => $id 68 | ); 69 | 70 | if ($revision !== false) $apipath["revision"] = $revision; 71 | 72 | $options = array(); 73 | if (!is_callable($bulkcallback)) $bulkcallback = false; 74 | if ($bulkcallback !== false) 75 | { 76 | $apipath["bulk"] = "1"; 77 | 78 | $options["read_body_callback"] = array($this, "Internal_BulkCallback"); 79 | $options["read_body_callback_opts"] = array("body" => "", "callback" => $bulkcallback); 80 | } 81 | 82 | if ($limit !== false) 83 | { 84 | $apipath["limit"] = (int)$limit; 85 | if ($offset !== false) $apipath["offset"] = (int)$offset; 86 | } 87 | 88 | return $this->RunAPI("GET", $apipath, $options, 200, true, ($bulkcallback === false)); 89 | } 90 | 91 | public static function ProcessInfoDefaults($info, $defaults) 92 | { 93 | foreach ($defaults as $key => $val) 94 | { 95 | if (!isset($info[$key])) $info[$key] = $val; 96 | } 97 | 98 | return $info; 99 | } 100 | 101 | public static function NormalizeAsset($info) 102 | { 103 | $defaults = array( 104 | "type" => "", "langinfo" => array(), "publish" => 0, "unpublish" => 0, 105 | "tags" => array(), "lastupdated" => 0, "lastupdatedby" => "", "lastip" => "", 106 | "files" => array(), "filesinfo" => array(), "protected" => array(), "uuid" => "" 107 | ); 108 | 109 | return self::ProcessInfoDefaults($info, $defaults); 110 | } 111 | 112 | private static function Internal_AppendGetAssetsOption(&$apipath, &$options, $key) 113 | { 114 | if (isset($options[$key])) 115 | { 116 | if (!is_array($options[$key])) $options[$key] = array($options[$key]); 117 | $items = array(); 118 | foreach ($options[$key] as $item) 119 | { 120 | if (is_string($item) || is_numeric($item)) $items[] = (string)$item; 121 | } 122 | 123 | if (count($items)) $apipath[$key . "[]"] = $items; 124 | } 125 | } 126 | 127 | // Valid options: 128 | // * tag - A string or an array of tags to search for. Prefix ~ to perform a partial match. Prefix ! to not allow the tag to match. 129 | // * id - A string or array of IDs to retrieve. Prefix ! to not match a given ID. 130 | // * uuid - A string or array of UUIDs to retrieve. Prefix ! to not match a given UUID. 131 | // * type - A string or array of types to retrieve. Prefix ! to not match a given type. 132 | // * q - A string or array of strings to search for a phrase match in the data blobs (can be slow). Prefix ! to not match a given query string. 133 | // * qe - A string or array of strings to search for an exact match in the data blobs (can be slow). Prefix ! to not match a given query string. 134 | // * start - An integer containing a UNIX timestamp. 135 | // * end - An integer containing a UNIX timestamp. 136 | // * order - A string containing one of 'lastupdated', 'unpublish', or 'publish' 137 | public function GetAssets($options, $limit = false, $bulkcallback = false) 138 | { 139 | $apipath = array( 140 | "ver" => "1", 141 | "ts" => time(), 142 | "api" => "assets" 143 | ); 144 | 145 | self::Internal_AppendGetAssetsOption($apipath, $options, "tag"); 146 | self::Internal_AppendGetAssetsOption($apipath, $options, "id"); 147 | self::Internal_AppendGetAssetsOption($apipath, $options, "uuid"); 148 | self::Internal_AppendGetAssetsOption($apipath, $options, "type"); 149 | self::Internal_AppendGetAssetsOption($apipath, $options, "q"); 150 | self::Internal_AppendGetAssetsOption($apipath, $options, "qe"); 151 | 152 | if (isset($options["start"]) && is_int($options["start"])) $apipath["start"] = (int)$options["start"]; 153 | if (isset($options["end"]) && is_int($options["end"])) $apipath["end"] = (int)$options["end"]; 154 | if (isset($options["order"]) && is_string($options["order"]) && ($options["order"] === "lastupdated" || $options["order"] === "unpublish" || $options["order"] === "publish")) $apipath["order"] = $options["order"]; 155 | if ($limit !== false && is_int($limit)) $apipath["limit"] = (int)$limit; 156 | 157 | $options = array(); 158 | if (!is_callable($bulkcallback)) $bulkcallback = false; 159 | if ($bulkcallback !== false) 160 | { 161 | $apipath["bulk"] = "1"; 162 | 163 | $options["read_body_callback"] = array($this, "Internal_BulkCallback"); 164 | $options["read_body_callback_opts"] = array("body" => "", "callback" => $bulkcallback); 165 | } 166 | 167 | return $this->RunAPI("GET", $apipath, $options, 200, true, ($bulkcallback === false)); 168 | } 169 | 170 | // Minimum requirements: 171 | // * type - A string. 172 | // * langinfo - An array containing at least one non-emtpy 'title'. 173 | public function StoreAsset($asset, $makerevision = true) 174 | { 175 | return $this->RunAPI("POST", array(), array("ver" => "1", "ts" => time(), "api" => "asset", "asset" => $this->NormalizeAsset($asset), "makerevision" => ($makerevision ? "1" : "0"))); 176 | } 177 | 178 | public function DeleteAsset($id) 179 | { 180 | $apipath = array( 181 | "ver" => "1", 182 | "ts" => time(), 183 | "api" => "delete_asset", 184 | "id" => $id 185 | ); 186 | 187 | return $this->RunAPI("GET", $apipath, array(), 200, false); 188 | } 189 | 190 | // Valid options: 191 | // * tag - A string to search for. Prefix ~ to perform a partial match. 192 | // * start - An integer containing a UNIX timestamp. 193 | // * end - An integer containing a UNIX timestamp. 194 | // * order - A string containing one of 'tag', 'publish', or 'numtags' 195 | public function GetTags($options, $limit = false, $offset = false, $bulkcallback = false) 196 | { 197 | $apipath = array( 198 | "ver" => "1", 199 | "ts" => time(), 200 | "api" => "tags" 201 | ); 202 | 203 | if (isset($options["tag"]) && is_string($options["tag"])) $apipath["tag"] = $options["tag"]; 204 | if (isset($options["start"]) && is_int($options["start"])) $apipath["start"] = (int)$options["start"]; 205 | if (isset($options["end"]) && is_int($options["end"])) $apipath["end"] = (int)$options["end"]; 206 | if (isset($options["order"]) && is_string($options["order"]) && ($options["order"] === "tag" || $options["order"] === "publish" || $options["order"] === "numtags")) $apipath["order"] = $options["order"]; 207 | if ($limit !== false && is_int($limit)) $apipath["limit"] = (int)$limit; 208 | if ($offset !== false && is_int($offset)) $apipath["offset"] = (int)$offset; 209 | 210 | $options = array(); 211 | if (!is_callable($bulkcallback)) $bulkcallback = false; 212 | if ($bulkcallback !== false) 213 | { 214 | $apipath["bulk"] = "1"; 215 | 216 | $options["read_body_callback"] = array($this, "Internal_BulkCallback"); 217 | $options["read_body_callback_opts"] = array("body" => "", "callback" => $bulkcallback); 218 | } 219 | 220 | return $this->RunAPI("GET", $apipath, $options, 200, true, ($bulkcallback === false)); 221 | } 222 | 223 | public function StartUpload($id, $filename, $lastupdatedby = "", $makerevision = true) 224 | { 225 | $options = array( 226 | "ver" => "1", 227 | "ts" => time(), 228 | "api" => "upload_start", 229 | "id" => (string)$id, 230 | "filename" => (string)$filename, 231 | "lastupdatedby" => $lastupdatedby, 232 | "makerevision" => ($makerevision ? "1" : "0") 233 | ); 234 | 235 | return $this->RunAPI("POST", array(), $options); 236 | } 237 | 238 | public function UploadFile($id, $filename, $start, $data) 239 | { 240 | $options = array( 241 | "postvars" => array( 242 | "ver" => "1", 243 | "ts" => (string)time(), 244 | "api" => "upload", 245 | "id" => (string)$id, 246 | "filename" => $filename, 247 | "start" => (string)$start 248 | ), 249 | "files" => array( 250 | array( 251 | "name" => "file", 252 | "filename" => $filename, 253 | "type" => "application/octet-stream", 254 | "data" => $data 255 | ) 256 | ) 257 | ); 258 | 259 | return $this->RunAPI("POST", array(), $options, 200, false); 260 | } 261 | 262 | public function UploadDone($id, $filename) 263 | { 264 | $options = array( 265 | "ver" => "1", 266 | "ts" => time(), 267 | "api" => "upload_done", 268 | "id" => (string)$id, 269 | "filename" => (string)$filename 270 | ); 271 | 272 | return $this->RunAPI("POST", array(), $options); 273 | } 274 | 275 | public function Internal_DownloadCallback($response, $body, &$opts) 276 | { 277 | if ($response["code"] == 200) 278 | { 279 | if (is_resource($opts[0])) fwrite($opts[0], $body); 280 | else if (is_callable($opts[0])) call_user_func_array($opts[0], array($opts[1], $body)); 281 | } 282 | 283 | return true; 284 | } 285 | 286 | // Depending on what happens with file uploads on the server (e.g. a process that moves uploaded files to a CDN), this may or may not work. 287 | // Useful for dealing with files managed by Remoted API Server or where additional processing is required (e.g. resizing/cropping images). 288 | // A local file cache is highly recommended to avoid using the API for repeated binary data retrieval. 289 | public function DownloadFile($id, $filename, $fpcallback = false, $callbackopts = false, $start = 0, $size = false, $recvratelimit = false) 290 | { 291 | $apipath = array( 292 | "ver" => "1", 293 | "ts" => time(), 294 | "api" => "file", 295 | "id" => (string)$id, 296 | "filename" => (string)$filename 297 | ); 298 | 299 | if ($start > 0) $apipath["start"] = $start; 300 | if ($size !== false) $apipath["size"] = $size; 301 | 302 | $options = array(); 303 | if ($fpcallback !== false) 304 | { 305 | $options["read_body_callback"] = array($this, "Internal_DownloadCallback"); 306 | $options["read_body_callback_opts"] = array($fpcallback, $callbackopts); 307 | } 308 | if ($recvratelimit !== false) $options["recvratelimit"] = $recvratelimit; 309 | 310 | return $this->RunAPI("GET", $apipath, $options, 200, true, false); 311 | } 312 | 313 | public function DeleteUpload($id, $filename, $lastupdatedby = "", $makerevision = true) 314 | { 315 | $apipath = array( 316 | "ver" => "1", 317 | "ts" => time(), 318 | "api" => "delete_upload", 319 | "id" => $id, 320 | "filename" => $filename, 321 | "lastupdatedby" => $lastupdatedby, 322 | "makerevision" => ($makerevision ? "1" : "0") 323 | ); 324 | 325 | return $this->RunAPI("GET", $apipath, array(), 200, false); 326 | } 327 | 328 | public static function GetPreferredLanguage($acceptlangs, $defaultlang) 329 | { 330 | $langs = array(); 331 | $langs2 = array(); 332 | $acceptlangs = explode(",", strtolower($acceptlangs)); 333 | foreach ($acceptlangs as $lang) 334 | { 335 | $lang = trim($lang); 336 | $pos = strpos($lang, ";"); 337 | if ($pos === false) $q = 100; 338 | else 339 | { 340 | $q = trim(substr($lang, $pos + 1)); 341 | $lang = substr($lang, 0, $pos); 342 | 343 | if (substr($q, 0, 2) == "q=") $q = (int)(trim(substr($q, 2)) * 100); 344 | } 345 | 346 | $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", trim($lang)))); 347 | 348 | if ($lang !== "" && !isset($langs2[$lang])) 349 | { 350 | while (isset($langs[$q])) $q--; 351 | 352 | $langs[$q] = $lang; 353 | $langs2[$lang] = true; 354 | } 355 | } 356 | krsort($langs); 357 | 358 | // Look for an exact match in language preference order. 359 | foreach ($langs as $lang) 360 | { 361 | return $lang; 362 | } 363 | 364 | return $defaultlang; 365 | } 366 | 367 | public static function GetPreferredAssetLanguage($asset, $acceptlangs, $defaultlangs, $fallbackfirstlang = true, $removestrs = array()) 368 | { 369 | if (!isset($asset["langinfo"])) return false; 370 | 371 | // If the asset only has one language, then return that. 372 | if (count($asset["langinfo"]) < 2) 373 | { 374 | foreach ($asset["langinfo"] as $lang => &$info) return $lang; 375 | 376 | return false; 377 | } 378 | 379 | // Parse the list of languages from Accept-Language (e.g. Accept-Language: fr-CH, fr;q=0.9, de;q=0.7, en;q=0.8, *;q=0.5). 380 | $langs = array(); 381 | $langs2 = array(); 382 | $acceptlangs = explode(",", strtolower($acceptlangs)); 383 | foreach ($acceptlangs as $lang) 384 | { 385 | $lang = trim($lang); 386 | $pos = strpos($lang, ";"); 387 | if ($pos === false) $q = 100; 388 | else 389 | { 390 | $q = trim(substr($lang, $pos + 1)); 391 | $lang = substr($lang, 0, $pos); 392 | 393 | if (substr($q, 0, 2) == "q=") $q = (int)(trim(substr($q, 2)) * 100); 394 | } 395 | 396 | $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", trim($lang)))); 397 | 398 | // Remove strings (e.g. internal language suffixes) found in user-submitted Accept-Language strings (e.g. '-newsletter' would be removed from 'en-us-newsletter'). 399 | $found = false; 400 | foreach ($removestrs as $str) 401 | { 402 | if (stripos($lang, $str) !== false) 403 | { 404 | $lang = str_ireplace($str, "", $lang); 405 | 406 | $found = true; 407 | } 408 | } 409 | 410 | if ($found) $lang = preg_replace('/\s+/', "-", trim(preg_replace('/[^a-z0-9]/', " ", $lang))); 411 | 412 | if ($lang !== "" && !isset($langs2[$lang])) 413 | { 414 | while (isset($langs[$q])) $q--; 415 | 416 | $langs[$q] = $lang; 417 | $langs2[$lang] = true; 418 | } 419 | } 420 | krsort($langs); 421 | 422 | // Look for an exact match in language preference order. 423 | foreach ($langs as $lang) 424 | { 425 | if (isset($asset["langinfo"][$lang])) return $lang; 426 | } 427 | 428 | // Look for a partial hyphenated match (e.g. 'en' is a 66.67% partial match for 'en-us' - that is, two out of three segments match). 429 | $partiallang = false; 430 | $partialpercent = 0; 431 | $langs2 = array(); 432 | foreach ($asset["langinfo"] as $lang2 => &$info) $langs2[$lang2] = explode("-", $lang2); 433 | foreach ($langs as $lang) 434 | { 435 | $words = explode("-", $lang); 436 | $numwords = count($words); 437 | 438 | foreach ($langs2 as $lang2 => $words2) 439 | { 440 | $y = min($numwords, count($words2)); 441 | for ($x = 0; $x < $y && $words[$x] === $words2[$x]; $x++) 442 | { 443 | } 444 | 445 | if ($x) 446 | { 447 | $percent = ($x * 2) / ($numwords + count($words2)); 448 | if ($partialpercent < $percent) 449 | { 450 | $partiallang = $lang2; 451 | $partialpercent = $percent; 452 | } 453 | } 454 | } 455 | } 456 | 457 | if ($partiallang !== false) return $partiallang; 458 | 459 | // A default language, if it exists. 460 | if (!is_array($defaultlangs)) $defaultlangs = array($defaultlangs); 461 | foreach ($defaultlangs as $lang) 462 | { 463 | if (isset($asset["langinfo"][$lang])) return $lang; 464 | } 465 | 466 | // First available option. 467 | if ($fallbackfirstlang) 468 | { 469 | foreach ($asset["langinfo"] as $lang => &$info) return $lang; 470 | } 471 | 472 | return false; 473 | } 474 | 475 | public static function GetPreferredTag($tags, $prefix, $overrideprefix = false) 476 | { 477 | $preftag = false; 478 | $y = strlen($prefix); 479 | if ($overrideprefix !== false) $y2 = strlen($overrideprefix); 480 | foreach ($tags as $tag) 481 | { 482 | if ($preftag === false && !strncmp($tag, $prefix, $y)) $preftag = $tag; 483 | else if ($overrideprefix !== false && !strncmp($tag, $overrideprefix, $y2)) $preftag = substr($tag, 1); 484 | } 485 | 486 | return $preftag; 487 | } 488 | 489 | public static function GetDefaultMimeInfoMap() 490 | { 491 | $result = array( 492 | ".jpg" => array("type" => "image/jpeg", "preview" => true), 493 | ".jpeg" => array("type" => "image/jpeg", "preview" => true), 494 | ".png" => array("type" => "image/png", "preview" => true), 495 | ".gif" => array("type" => "image/gif", "preview" => true), 496 | ".svg" => array("type" => "image/svg+xml", "preview" => false), 497 | ".mp3" => array("type" => "audio/mpeg", "preview" => true), 498 | ".wav" => array("type" => "audio/wav", "preview" => true), 499 | ".mp4" => array("type" => "video/mp4", "preview" => true), 500 | ".webm" => array("type" => "video/webm", "preview" => true), 501 | ".pdf" => array("type" => "application/pdf", "preview" => false), 502 | ); 503 | 504 | return $result; 505 | } 506 | 507 | public static function GetFileExtension($filename) 508 | { 509 | $pos = strrpos($filename, "."); 510 | if ($pos === false) return ""; 511 | 512 | return substr($filename, $pos); 513 | } 514 | 515 | // Generates a digital signature to prevent abuse of system resources (image manipulation operations use a lot of CPU and cache storage might be limited). 516 | public static function GetFileSignature($id, $path, $filename, $crop, $maxwidth, $secret) 517 | { 518 | return hash_hmac("sha1", (string)$id . "|" . (string)$path . "|" . (string)$filename . "|" . (string)$crop . "|" . (string)$maxwidth, $secret); 519 | } 520 | 521 | public static function IsValidFileSignature($id, $path, $filename, $crop, $maxwidth, $signature, $secret) 522 | { 523 | return (self::GetFileSignature($id, $path, $filename, $crop, $maxwidth, $secret) === $signature); 524 | } 525 | 526 | // Produces results that are similar to the API. 527 | public static function SanitizeFilename($filename) 528 | { 529 | return preg_replace('/\s+/', "-", trim(trim(preg_replace('/[^A-Za-z0-9_.\-~]/', " ", $filename), "."))); 530 | } 531 | 532 | public static function CanResizeImage($mimeinfomap, $fileext) 533 | { 534 | return (isset($mimeinfomap[$fileext]) && ($mimeinfomap[$fileext]["type"] === "image/jpeg" || $mimeinfomap[$fileext]["type"] === "image/png" || $mimeinfomap[$fileext]["type"] === "image/gif")); 535 | } 536 | 537 | public static function GetCroppedAndScaledFilename($filename, $fileext, $crop, $maxwidth) 538 | { 539 | $crop = explode(",", preg_replace('/[^0-9.,]/', "", $crop)); 540 | 541 | if (count($crop) != 4 || $crop[0] === $crop[2] || $crop[1] === $crop[3] || $crop[0] < 0 || $crop[1] < 0 || $crop[2] < 0 || $crop[3] < 0) $crop = ""; 542 | else 543 | { 544 | // Truncate percentage crops to 5 decimal places. 545 | foreach ($crop as $num => $val) 546 | { 547 | $pos = strpos($val, "."); 548 | if ($pos !== false) $crop[$num] = substr($val, 0, $pos + 6); 549 | } 550 | 551 | $crop = implode(",", $crop); 552 | } 553 | 554 | $result = substr($filename, 0, -strlen($fileext)) . "." . ($crop !== "" ? $crop . "_" : "") . (int)$maxwidth . $fileext; 555 | 556 | return $result; 557 | } 558 | 559 | public static function GetDestCropAndSize(&$cropx, &$cropy, &$cropw, &$croph, &$destwidth, &$destheight, $srcwidth, $srcheight, $crop, $maxwidth, $maxheight) 560 | { 561 | $cropx = 0; 562 | $cropy = 0; 563 | $cropw = (int)$srcwidth; 564 | $croph = (int)$srcheight; 565 | 566 | if ($crop !== "") 567 | { 568 | $crop = explode(",", preg_replace('/[^0-9.,]/', "", $crop)); 569 | if (count($crop) == 4 && $crop[0] !== $crop[2] && $crop[1] !== $crop[3] && $crop[0] >= 0 && $crop[1] >= 0 && $crop[2] >= 0 && $crop[3] >= 0 && $crop[0] < $srcwidth && $crop[1] < $srcheight && $crop[2] < $srcwidth && $crop[3] < $srcheight) 570 | { 571 | $cropx = (double)$crop[0]; 572 | $cropy = (double)$crop[1]; 573 | $cropw = (double)$crop[2]; 574 | $croph = (double)$crop[3]; 575 | 576 | // Normalize. 577 | if ($cropx > $cropw) 578 | { 579 | $temp = $cropx; 580 | $cropx = $cropw; 581 | $cropw = $temp; 582 | } 583 | if ($cropy > $croph) 584 | { 585 | $temp = $cropy; 586 | $cropy = $croph; 587 | $croph = $temp; 588 | } 589 | if ($cropw < 1.00001 && $croph < 1.00001) 590 | { 591 | // Assume percentage of the image. 592 | $cropx = $cropx * $srcwidth; 593 | $cropy = $cropy * $srcheight; 594 | $cropw = $cropw * $srcwidth; 595 | $croph = $croph * $srcheight; 596 | } 597 | 598 | $cropw = (int)($cropw - $cropx); 599 | $croph = (int)($croph - $cropy); 600 | $cropx = (int)$cropx; 601 | $cropy = (int)$cropy; 602 | } 603 | } 604 | 605 | // Calculate final image width and height. 606 | if ($cropw <= $maxwidth) 607 | { 608 | $destwidth = $cropw; 609 | $destheight = $croph; 610 | } 611 | else 612 | { 613 | $destwidth = $maxwidth; 614 | $destheight = (int)($croph * $destwidth / $cropw); 615 | } 616 | 617 | if ($destheight > $maxheight) 618 | { 619 | $destwidth = (int)($destwidth * $maxheight / $destheight); 620 | $destheight = $maxheight; 621 | } 622 | } 623 | 624 | public static function CropAndScaleImage($data, $crop = "", $maxwidth = false, $maxheight = false) 625 | { 626 | @ini_set("memory_limit", "512M"); 627 | 628 | // Detect which image library is available to crop and scale the image. 629 | if (extension_loaded("imagick")) 630 | { 631 | // ImageMagick. 632 | try 633 | { 634 | $img = new Imagick(); 635 | $img->readImageBlob($data); 636 | $info = $img->getImageGeometry(); 637 | $srcwidth = $info["width"]; 638 | $srcheight = $info["height"]; 639 | 640 | if ($maxwidth === false) $maxwidth = $srcwidth; 641 | if ($maxheight === false) $maxheight = $srcheight; 642 | 643 | if ($srcwidth <= $maxwidth && $crop === "") return array("success" => true, "data" => $data); 644 | 645 | // Calculate various points. 646 | self::GetDestCropAndSize($cropx, $cropy, $cropw, $croph, $destwidth, $destheight, $srcwidth, $srcheight, $crop, $maxwidth, $maxheight); 647 | } 648 | catch (Exception $e) 649 | { 650 | return array("success" => false, "error" => self::CMS_Translate("Unable to load image."), "errorcode" => "image_load_failed"); 651 | } 652 | 653 | try 654 | { 655 | // Crop the image. 656 | if ($crop !== "") 657 | { 658 | $img->cropImage($cropw, $croph, $cropx, $cropy); 659 | 660 | // Strip out EXIF and 8BIM profiles (if any) since embedded thumbnails no longer match the actual image. 661 | $profiles = $img->getImageProfiles("*", false); 662 | foreach ($profiles as $profile) 663 | { 664 | if ($profile === "exif" || $profile === "8bim") $img->removeImageProfile($profile); 665 | } 666 | } 667 | 668 | // Resize the image. 669 | $img->resizeImage($destwidth, $destheight, imagick::FILTER_CATROM, 1); 670 | 671 | // Gather the result. 672 | return array("success" => true, "data" => $img->getImageBlob()); 673 | } 674 | catch (Exception $e) 675 | { 676 | return array("success" => false, "error" => self::CMS_Translate("Unable to crop/resize image."), "errorcode" => "image_crop_resize_failed"); 677 | } 678 | } 679 | else if (extension_loaded("gd") && function_exists("gd_info")) 680 | { 681 | // GD. 682 | $info = @getimagesizefromstring($data); 683 | if ($info === false) return array("success" => false, "error" => self::CMS_Translate("Unable to load image."), "errorcode" => "image_load_failed"); 684 | $srcwidth = $info[0]; 685 | $srcheight = $info[1]; 686 | $type = $info[2]; 687 | 688 | if ($type !== IMAGETYPE_JPEG && $type !== IMAGETYPE_PNG && $type !== IMAGETYPE_GIF) return array("success" => false, "error" => self::CMS_Translate("Unsupported image format."), "errorcode" => "unsupported_image_format"); 689 | 690 | if ($maxwidth === false) $maxwidth = $srcwidth; 691 | if ($maxheight === false) $maxheight = $srcheight; 692 | 693 | if ($srcwidth <= $maxwidth && $crop === "") return array("success" => true, "data" => $data); 694 | 695 | // Calculate various points. 696 | self::GetDestCropAndSize($cropx, $cropy, $cropw, $croph, $destwidth, $destheight, $srcwidth, $srcheight, $crop, $maxwidth, $maxheight); 697 | 698 | $img = @imagecreatefromstring($data); 699 | if ($img === false) return array("success" => false, "error" => self::CMS_Translate("Unable to load image."), "errorcode" => "image_load_failed"); 700 | $data = ""; 701 | 702 | $img2 = @imagecreatetruecolor($destwidth, $destheight); 703 | if ($img2 === false) 704 | { 705 | imagedestroy($img); 706 | 707 | return array("success" => false, "error" => self::CMS_Translate("Unable to crop/resize image."), "errorcode" => "image_crop_resize_failed"); 708 | } 709 | 710 | // Make fully transparent (if relevant). 711 | if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF) 712 | { 713 | $transparent = imagecolorallocatealpha($img2, 0, 0, 0, 127); 714 | imagecolortransparent($img2, $transparent); 715 | imagealphablending($img2, false); 716 | imagesavealpha($img2, true); 717 | imagefill($img2, 0, 0, $transparent); 718 | } 719 | 720 | // Copy the source onto the destination, resizing in the process. 721 | imagecopyresampled($img2, $img, 0, 0, $cropx, $cropy, $destwidth, $destheight, $cropw, $croph); 722 | imagedestroy($img); 723 | 724 | ob_start(); 725 | if ($type === IMAGETYPE_JPEG) @imagejpeg($img2, NULL, 85); 726 | else if ($type === IMAGETYPE_PNG) @imagepng($img2); 727 | else if ($type === IMAGETYPE_GIF) @imagegif($img2); 728 | $data = ob_get_contents(); 729 | ob_end_clean(); 730 | 731 | imagedestroy($img2); 732 | 733 | return array("success" => true, "data" => $data); 734 | } 735 | 736 | return array("success" => false, "error" => self::CMS_Translate("A supported image library is not installed/configured."), "errorcode" => "missing_image_library"); 737 | } 738 | 739 | public function Internal_DeliverFileDownloadCallback($state, $body) 740 | { 741 | if ($state->size > 0) 742 | { 743 | if (strlen($body) > $state->size) $body = substr($body, 0, $state->size); 744 | 745 | if ($body !== "") 746 | { 747 | if (!$state->cacheonly) 748 | { 749 | if ($state->body !== false) $state->body .= $body; 750 | else if (!connection_aborted()) echo $body; 751 | } 752 | 753 | if ($state->cachefp !== false) fwrite($state->cachefp, $body); 754 | 755 | $state->size -= strlen($body); 756 | } 757 | } 758 | } 759 | 760 | // Swiped from 'http.php'. 761 | private static function ProcessRateLimit($size, $start, $limit, $async) 762 | { 763 | $difftime = microtime(true) - $start; 764 | if ($difftime > 0.0) 765 | { 766 | if ($size / $difftime > $limit) 767 | { 768 | // Sleeping for some amount of time will equalize the rate. 769 | // So, solve this for $x: $size / ($x + $difftime) = $limit 770 | $amount = ($size - ($limit * $difftime)) / $limit; 771 | $amount += 0.001; 772 | 773 | if ($async) return microtime(true) + $amount; 774 | else usleep($amount * 1000000); 775 | } 776 | } 777 | 778 | return -1.0; 779 | } 780 | 781 | // A powerful function for handling requests for file content from a web browser with local caching, cropping/scaling (images), partial data (Range requests), and rate limiting options. 782 | public function DeliverFile($id, $filename, $options) 783 | { 784 | // Normalize options. 785 | if (!isset($options["cachedir"]) || !is_string($options["cachedir"]) || !is_dir($options["cachedir"])) $options["cachedir"] = false; 786 | if (!isset($options["apidir"]) || !is_string($options["apidir"]) || !is_dir($options["apidir"])) $options["apidir"] = false; 787 | if (!isset($options["maxcachefilesize"]) || !is_numeric($options["maxcachefilesize"])) $options["maxcachefilesize"] = 10000000; 788 | if (!isset($options["path"]) || !is_string($options["path"])) $options["path"] = ""; 789 | $options["path"] = preg_replace('/[^0-9]/', "", $options["path"]); 790 | if (!isset($options["download"]) || !is_string($options["download"])) $options["download"] = false; 791 | else $options["download"] = preg_replace('/\s+/', "-", trim(trim(preg_replace('/[^A-Za-z0-9_.\-\x80-\xff]/', " ", $options["download"]), "."))); 792 | if (!isset($options["crop"]) || !is_string($options["crop"])) $options["crop"] = ""; 793 | if (!isset($options["maxwidth"]) || !is_numeric($options["maxwidth"])) $options["maxwidth"] = -1; 794 | if (!isset($options["mimeinfomap"]) || !is_array($options["mimeinfomap"])) $options["mimeinfomap"] = self::GetDefaultMimeInfoMap(); 795 | if (!isset($options["recvratelimit"]) || $options["recvratelimit"] < 1) $options["recvratelimit"] = false; 796 | if (!isset($options["cacheonly"]) || !is_bool($options["cacheonly"])) $options["cacheonly"] = false; 797 | 798 | // Process filename. 799 | $filename = self::SanitizeFilename($filename); 800 | $fileext = self::GetFileExtension($filename); 801 | $isimage = self::CanResizeImage($options["mimeinfomap"], $fileext); 802 | if ($options["download"] === "") $options["download"] = $filename; 803 | 804 | // Handle final filename generation for scaled/cropped images. 805 | if (!$isimage || $options["maxwidth"] < 1) $finalfilename = $filename; 806 | else $finalfilename = self::GetCroppedAndScaledFilename($filename, $fileext, $options["crop"], $options["maxwidth"]); 807 | 808 | // Check for an already cached/local file to avoid querying the API. 809 | $final = ($finalfilename === $filename); 810 | do 811 | { 812 | $filesrc = "api"; 813 | $filesrcname = $filename; 814 | $mainfp = false; 815 | $size = false; 816 | $ts = false; 817 | $retry = false; 818 | 819 | if ($options["path"] != "" && $options["cachedir"] !== false && is_file($options["cachedir"] . "/" . $options["path"] . "/" . $filename . ".dat")) 820 | { 821 | $srcdata = @json_decode(file_get_contents($options["cachedir"] . "/" . $options["path"] . "/" . $filename . ".dat"), true); 822 | if (is_array($srcdata) && isset($srcdata["modified"]) && is_file($options["cachedir"] . "/" . $options["path"] . "/" . $finalfilename) && filemtime($options["cachedir"] . "/" . $options["path"] . "/" . $finalfilename) < time() - 1) 823 | { 824 | $filesrc = "cachedir"; 825 | $filesrcname = $finalfilename; 826 | $mainfp = $options["cachedir"] . "/" . $options["path"] . "/" . $finalfilename; 827 | $final = true; 828 | 829 | $size = filesize($mainfp); 830 | $ts = $srcdata["modified"]; 831 | } 832 | else if (is_array($srcdata) && isset($srcdata["size"]) && isset($srcdata["modified"]) && is_file($options["cachedir"] . "/" . $options["path"] . "/" . $filename) && filesize($options["cachedir"] . "/" . $options["path"] . "/" . $filename) >= $srcdata["size"]) 833 | { 834 | $filesrc = "cachedir"; 835 | $mainfp = $options["cachedir"] . "/" . $options["path"] . "/" . $filename; 836 | $size = $srcdata["size"]; 837 | $ts = $srcdata["modified"]; 838 | } 839 | } 840 | if ($mainfp === false && $options["path"] != "" && ($options["apidir"] !== false && is_file($options["apidir"] . "/" . $options["path"] . "/" . $filename))) 841 | { 842 | $filesrc = "apidir"; 843 | $mainfp = $options["apidir"] . "/" . $options["path"] . "/" . $filename; 844 | $size = filesize($mainfp); 845 | $ts = filemtime($mainfp); 846 | } 847 | 848 | // Retrieve information about the file. 849 | $asset = (is_array($id) && isset($id["files"]) ? $id : false); 850 | if ($asset !== false) $id = (string)$asset["id"]; 851 | else if ($mainfp === false) 852 | { 853 | $result = $this->GetAssets(array("id" => (string)$id)); 854 | if (!$result["success"]) 855 | { 856 | // API failure. 857 | if ($options["cacheonly"]) return $result; 858 | 859 | http_response_code(502); 860 | 861 | exit(); 862 | } 863 | else if (!count($result["assets"])) 864 | { 865 | // Failed to retrieve the asset. 866 | if ($options["cacheonly"]) return array("success" => false, "error" => self::CMS_Translate("Unable to locate the requested asset."), "errorcode" => "asset_not_found"); 867 | 868 | http_response_code(404); 869 | 870 | exit(); 871 | } 872 | 873 | $asset = self::NormalizeAsset($result["assets"][0]); 874 | } 875 | 876 | if ($mainfp ===false && $asset !== false) 877 | { 878 | if (!isset($asset["files"]) || !isset($asset["files"][$filename])) 879 | { 880 | // The asset exists but does not contain the requested file. 881 | if ($options["cacheonly"]) return array("success" => false, "error" => self::CMS_Translate("Asset does not contain the requested file."), "errorcode" => "asset_file_not_found"); 882 | 883 | http_response_code(404); 884 | 885 | exit(); 886 | } 887 | 888 | $prevpath = $options["path"]; 889 | $options["path"] = preg_replace('/[^0-9]/', "", $asset["files"][$filename]["path"]); 890 | if ($prevpath !== $options["path"]) 891 | { 892 | $id = $asset; 893 | 894 | $retry = true; 895 | } 896 | else 897 | { 898 | $size = $asset["files"][$filename]["size"]; 899 | $ts = $asset["files"][$filename]["modified"]; 900 | } 901 | } 902 | } while ($retry); 903 | 904 | // Shortcut for cache only calls to return immediately when the file is already on disk. 905 | if ($mainfp !== false && $options["cacheonly"] && $final) 906 | { 907 | $result = array( 908 | "success" => true, 909 | "filesrc" => $filesrc, 910 | "path" => $options["path"], 911 | "filename" => $filesrcname 912 | ); 913 | 914 | return $result; 915 | } 916 | 917 | // Handle HEAD requests (i.e. just wanting to know information). 918 | if (!$options["cacheonly"] && strtoupper($_SERVER["REQUEST_METHOD"]) === "HEAD") 919 | { 920 | // Return HTTP 405 if the resource is a supported resizable image file that doesn't exist yet. 921 | if ($isimage && !$final) http_response_code(405); 922 | else 923 | { 924 | if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && HTTP::GetDateTimestamp($_SERVER["HTTP_IF_MODIFIED_SINCE"]) === $ts) http_response_code(304); 925 | 926 | if ($options["download"] === false && isset($options["mimeinfomap"][$fileext])) header("Content-Type: " . $options["mimeinfomap"][$fileext]["type"]); 927 | else 928 | { 929 | header("Content-Type: application/octet-stream"); 930 | header("Content-Disposition: attachment; filename=\"" . $options["download"] . "\""); 931 | } 932 | 933 | header("Accept-Ranges: bytes"); 934 | 935 | if (http_response_code() !== 304) 936 | { 937 | header("Last-Modified: " . gmdate("D, d M Y H:i:s", $ts) . " GMT"); 938 | header("Content-Length: " . $size); 939 | } 940 | } 941 | 942 | exit(); 943 | } 944 | 945 | // Calculate the amount of data to transfer. Only implement partial support for the Range header (coalesce requests into a single range). 946 | $start = 0; 947 | $origsize = $size; 948 | if (!$options["cacheonly"] && (!$isimage || $final) && isset($_SERVER["HTTP_RANGE"]) && $size > 0) 949 | { 950 | $min = false; 951 | $max = false; 952 | $ranges = explode(";", $_SERVER["HTTP_RANGE"]); 953 | foreach ($ranges as $range) 954 | { 955 | $range = explode("=", trim($range)); 956 | if (count($range) > 1 && strtolower($range[0]) === "bytes") 957 | { 958 | $chunks = explode(",", $range[1]); 959 | foreach ($chunks as $chunk) 960 | { 961 | $chunk = explode("-", trim($chunk)); 962 | if (count($chunk) == 2) 963 | { 964 | $pos = trim($chunk[0]); 965 | $pos2 = trim($chunk[1]); 966 | 967 | if ($pos === "" && $pos2 === "") 968 | { 969 | // Ignore invalid range. 970 | } 971 | else if ($pos === "") 972 | { 973 | if ($min === false || $min > $size - (int)$pos) $min = $size - (int)$pos; 974 | } 975 | else if ($pos2 === "") 976 | { 977 | if ($min === false || $min > (int)$pos) $min = (int)$pos; 978 | } 979 | else 980 | { 981 | if ($min === false || $min > (int)$pos) $min = (int)$pos; 982 | if ($max === false || $max < (int)$pos2) $max = (int)$pos2; 983 | } 984 | } 985 | } 986 | } 987 | } 988 | 989 | // Normalize and cap byte ranges. 990 | if ($min === false) $min = 0; 991 | if ($max === false) $max = $size - 1; 992 | if ($min < 0) $min = 0; 993 | if ($min > $size - 1) $min = $size - 1; 994 | if ($max < 0) $max = 0; 995 | if ($max > $size - 1) $max = $size - 1; 996 | if ($max < $min) $max = $min; 997 | 998 | // Translate to start and size. 999 | $start = $min; 1000 | $size = $max - $min + 1; 1001 | } 1002 | 1003 | // Begin writing local cache information to disk if the file extension is a supported mime type, the file doesn't exceed the local cache file size limit, and this isn't a If-Modified-Since or byte Range request. 1004 | $state = new stdClass(); 1005 | $state->cacheonly = false; 1006 | $state->cachefp = false; 1007 | if ($mainfp === false && isset($options["mimeinfomap"][$fileext]) && $options["cachedir"] !== false && $options["apidir"] === false && $asset !== false && $asset["files"][$filename]["size"] <= $options["maxcachefilesize"] && !isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && $start === 0 && $size === $origsize) 1008 | { 1009 | @mkdir($options["cachedir"] . "/" . $options["path"]); 1010 | if (!file_exists($options["cachedir"] . "/" . $options["path"] . "/index.html")) @file_put_contents($options["cachedir"] . "/" . $options["path"] . "/index.html", ""); 1011 | @file_put_contents($options["cachedir"] . "/" . $options["path"] . "/" . $filename . ".dat", json_encode($asset["files"][$filename], JSON_UNESCAPED_SLASHES)); 1012 | 1013 | $state->cachefp = @fopen($options["cachedir"] . "/" . $options["path"] . "/" . $filename, "wb"); 1014 | if ($state->cachefp !== false) $filesrc = "cachedir"; 1015 | 1016 | // Turn off user abort when writing to the local cache (avoids partial writes). 1017 | @ignore_user_abort(true); 1018 | } 1019 | 1020 | // Carefully clear out various PHP restrictions. 1021 | @set_time_limit(0); 1022 | 1023 | if (!$options["cacheonly"]) 1024 | { 1025 | @ob_clean(); 1026 | if (function_exists("apache_setenv")) @apache_setenv("no-gzip", 1); 1027 | @ini_set("zlib.output_compression", "Off"); 1028 | } 1029 | 1030 | // Open the file for reading. 1031 | if ($mainfp !== false) 1032 | { 1033 | $mainfp = @fopen($mainfp, "rb"); 1034 | if ($mainfp === false) 1035 | { 1036 | // Permissions failure. 1037 | if ($options["cacheonly"]) return array("success" => false, "error" => self::CMS_Translate("Unable to open file for reading."), "errorcode" => "access_denied"); 1038 | 1039 | http_response_code(550); 1040 | 1041 | exit(); 1042 | } 1043 | } 1044 | 1045 | $state->size = $size; 1046 | if ($isimage && !$final) 1047 | { 1048 | // Load the image data. 1049 | if ($mainfp !== false) 1050 | { 1051 | $state->body = fread($mainfp, $origsize); 1052 | 1053 | fclose($mainfp); 1054 | } 1055 | else 1056 | { 1057 | $state->body = ""; 1058 | $result = $this->DownloadFile($id, $filename, array($this, "Internal_DeliverFileDownloadCallback"), $state); 1059 | if (!$result["success"]) 1060 | { 1061 | // Cleanup and retry later. 1062 | if ($state->cachefp !== false) 1063 | { 1064 | fclose($state->cachefp); 1065 | 1066 | @unlink($options["cachedir"] . "/" . $options["path"] . "/" . $filename, $ts); 1067 | } 1068 | 1069 | // API failure. 1070 | if ($options["cacheonly"]) return array("success" => false, "error" => self::CMS_Translate("Unable to retrieve the file from the API."), "errorcode" => "api_failure"); 1071 | 1072 | http_response_code(502); 1073 | 1074 | exit(); 1075 | } 1076 | 1077 | if ($state->cachefp !== false) 1078 | { 1079 | fclose($state->cachefp); 1080 | 1081 | @touch($options["cachedir"] . "/" . $options["path"] . "/" . $filename, $ts); 1082 | 1083 | $state->cachefp = false; 1084 | } 1085 | } 1086 | 1087 | $result = self::CropAndScaleImage($state->body, $options["crop"], $options["maxwidth"]); 1088 | $state->body = false; 1089 | 1090 | if (!$result["success"]) 1091 | { 1092 | // A supported image library is not installed/configured. 1093 | if ($options["cacheonly"]) return $result; 1094 | 1095 | http_response_code(($result["errorcode"] === "missing_image_library" ? 501 : 500)); 1096 | 1097 | exit(); 1098 | } 1099 | 1100 | $mainfp = $result["data"]; 1101 | 1102 | // Write the resized file to disk (if caching). 1103 | if ($options["cachedir"] !== false && strlen($mainfp) <= $options["maxcachefilesize"]) 1104 | { 1105 | @mkdir($options["cachedir"] . "/" . $options["path"]); 1106 | if (!file_exists($options["cachedir"] . "/" . $options["path"] . "/index.html")) @file_put_contents($options["cachedir"] . "/" . $options["path"] . "/index.html", ""); 1107 | if ($asset !== false && !file_exists($options["cachedir"] . "/" . $options["path"] . "/" . $filename . ".dat")) @file_put_contents($options["cachedir"] . "/" . $options["path"] . "/" . $filename . ".dat", json_encode($asset["files"][$filename], JSON_UNESCAPED_SLASHES)); 1108 | if (@file_put_contents($options["cachedir"] . "/" . $options["path"] . "/" . $finalfilename, $mainfp) !== false) $filesrc = "cachedir"; 1109 | @touch($options["cachedir"] . "/" . $options["path"] . "/" . $finalfilename, $ts); 1110 | 1111 | if ($options["cacheonly"]) 1112 | { 1113 | if ($filesrc !== "cachedir") return array("success" => false, "error" => self::CMS_Translate("Unable to cache the file to disk."), "errorcode" => "access_denied"); 1114 | 1115 | $result = array( 1116 | "success" => true, 1117 | "filesrc" => $filesrc, 1118 | "path" => $options["path"], 1119 | "filename" => $finalfilename 1120 | ); 1121 | 1122 | return $result; 1123 | } 1124 | } 1125 | 1126 | $origsize = strlen($mainfp); 1127 | $size = $origsize; 1128 | } 1129 | 1130 | // Process cache only download, cleanup, and finalize. 1131 | if ($options["cacheonly"]) 1132 | { 1133 | if ($mainfp !== false) 1134 | { 1135 | if (!is_string($mainfp)) fclose($mainfp); 1136 | } 1137 | else if ($state->cachefp !== false) 1138 | { 1139 | $state->cacheonly = true; 1140 | $state->body = false; 1141 | $this->DownloadFile($id, $filename, array($this, "Internal_DeliverFileDownloadCallback"), $state, $start, $size, $options["recvratelimit"]); 1142 | 1143 | fclose($state->cachefp); 1144 | 1145 | @touch($options["cachedir"] . "/" . $options["path"] . "/" . $filename, $ts); 1146 | } 1147 | 1148 | $result = array( 1149 | "success" => true, 1150 | "filesrc" => $filesrc, 1151 | "path" => $options["path"], 1152 | "filename" => $filename 1153 | ); 1154 | 1155 | return $result; 1156 | } 1157 | 1158 | // Deliver the final content. 1159 | if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"]) && HTTP::GetDateTimestamp($_SERVER["HTTP_IF_MODIFIED_SINCE"]) === $ts) http_response_code(304); 1160 | else if ($start > 0 || $size !== $origsize) 1161 | { 1162 | http_response_code(206); 1163 | 1164 | header("Content-Range: bytes " . $start . "-" . $size . "/" . $origsize); 1165 | } 1166 | 1167 | if ($options["download"] === false && isset($options["mimeinfomap"][$fileext])) header("Content-Type: " . $options["mimeinfomap"][$fileext]["type"]); 1168 | else 1169 | { 1170 | header("Content-Type: application/octet-stream"); 1171 | header("Content-Disposition: attachment; filename=\"" . $options["download"] . "\""); 1172 | } 1173 | 1174 | header("Accept-Ranges: bytes"); 1175 | 1176 | if (http_response_code() !== 304) 1177 | { 1178 | header("Last-Modified: " . gmdate("D, d M Y H:i:s", $ts) . " GMT"); 1179 | header("Content-Length: " . $size); 1180 | 1181 | if ($mainfp !== false) 1182 | { 1183 | // Dump the data out. 1184 | $startts = microtime(true); 1185 | if (is_string($mainfp)) 1186 | { 1187 | $y = strlen($mainfp); 1188 | if ($options["recvratelimit"] === false) $x = 0; 1189 | else 1190 | { 1191 | for ($x = 0; $x + 65536 <= $y; $x += 65536) 1192 | { 1193 | echo substr($mainfp, $x, 65536); 1194 | 1195 | self::ProcessRateLimit($x, $startts, $options["recvratelimit"], false); 1196 | } 1197 | } 1198 | 1199 | if ($x < $y) echo substr($mainfp, $x); 1200 | } 1201 | else 1202 | { 1203 | fseek($mainfp, $start); 1204 | 1205 | $sentbytes = 0; 1206 | while ($size >= 65536) 1207 | { 1208 | echo fread($mainfp, 65536); 1209 | $size -= 65536; 1210 | $sentbytes += 65536; 1211 | 1212 | if ($options["recvratelimit"] !== false) self::ProcessRateLimit($sentbytes, $startts, $options["recvratelimit"], false); 1213 | } 1214 | 1215 | if ($size) echo fread($mainfp, $size); 1216 | 1217 | fclose($mainfp); 1218 | } 1219 | } 1220 | else 1221 | { 1222 | $state->body = false; 1223 | $this->DownloadFile($id, $filename, array($this, "Internal_DeliverFileDownloadCallback"), $state, $start, $size, $options["recvratelimit"]); 1224 | 1225 | if ($state->cachefp !== false) 1226 | { 1227 | fclose($state->cachefp); 1228 | 1229 | @touch($options["cachedir"] . "/" . $options["path"] . "/" . $filename, $ts); 1230 | } 1231 | } 1232 | } 1233 | 1234 | exit(); 1235 | } 1236 | 1237 | // A basic wrapper around DeliverFile() to precache files locally without emitting HTTP status codes or headers. 1238 | // Also determines the final file URL, if any, that should be used. 1239 | public function PrecacheDeliverFile($id, $filename, $options) 1240 | { 1241 | // Normalize options. 1242 | if (!isset($options["cachedir"]) || !is_string($options["cachedir"]) || !is_dir($options["cachedir"])) $options["cachedir"] = false; 1243 | if (!isset($options["cacheurl"]) || !is_string($options["cacheurl"]) || $options["cachedir"] === false) $options["cacheurl"] = false; 1244 | if (!isset($options["apidir"]) || !is_string($options["apidir"]) || !is_dir($options["apidir"])) $options["apidir"] = false; 1245 | if (!isset($options["apiurl"]) || !is_string($options["apiurl"]) || $options["apidir"] === false) $options["apiurl"] = false; 1246 | if (!isset($options["getfileurl"]) || !is_string($options["getfileurl"])) $options["getfileurl"] = false; 1247 | if (!isset($options["getfilesecret"]) || !is_string($options["getfilesecret"])) $options["getfilesecret"] = false; 1248 | if (!isset($options["download"]) || !is_string($options["download"])) $options["download"] = false; 1249 | 1250 | if ($options["cacheurl"] === false && $options["apiurl"] === false) 1251 | { 1252 | // There's no need to call DeliverFile() if it won't actually cache anything. 1253 | $result = array( 1254 | "success" => true, 1255 | "filesrc" => "api", 1256 | "path" => (isset($options["path"]) && is_string($options["path"]) ? $options["path"] : ""), 1257 | "filename" => $filename 1258 | ); 1259 | } 1260 | else 1261 | { 1262 | // Bypass browser file delivery. 1263 | $options["cacheonly"] = true; 1264 | 1265 | $result = $this->DeliverFile($id, $filename, $options); 1266 | if (!$result["success"]) return $result; 1267 | } 1268 | 1269 | // File in cache directory with public cache URL. 1270 | if ($result["filesrc"] === "cachedir" && $options["cacheurl"] !== false && $options["download"] === false) 1271 | { 1272 | if (substr($options["cacheurl"], -1) !== "/") $options["cacheurl"] .= "/"; 1273 | 1274 | return array("success" => true, "url" => $options["cacheurl"] . $result["path"] . "/" . $result["filename"]); 1275 | } 1276 | 1277 | // File in local API directory with public files API URL. 1278 | if ($result["filesrc"] === "apidir" && $options["apiurl"] !== false && $options["download"] === false) 1279 | { 1280 | if (substr($options["apiurl"], -1) !== "/") $options["apiurl"] .= "/"; 1281 | 1282 | return array("success" => true, "url" => $options["apiurl"] . $result["path"] . "/" . $result["filename"]); 1283 | } 1284 | 1285 | // All other requests with public file URL support. 1286 | if ($options["getfileurl"] !== false) 1287 | { 1288 | if (is_array($id)) $id = $id["id"]; 1289 | if (!isset($options["crop"]) || !is_string($options["crop"])) $options["crop"] = ""; 1290 | if (!isset($options["maxwidth"]) || !is_numeric($options["maxwidth"])) $options["maxwidth"] = -1; 1291 | 1292 | $url = $options["getfileurl"]; 1293 | $url .= (strpos($url, "?") === false ? "?" : "&"); 1294 | $url .= "id=" . urlencode($id) . "&path=" . urlencode($result["path"]) . "&filename=" . urlencode($result["filename"]); 1295 | if ($options["download"] !== false) $url .= "&download=" . urlencode($options["download"]); 1296 | if ($options["crop"] != "") $url .= "&crop=" . urlencode($options["crop"]); 1297 | if ($options["maxwidth"] > 0) $url .= "&maxwidth=" . (int)$options["maxwidth"]; 1298 | 1299 | if ($options["getfilesecret"] !== false) $url .= "&sig=" . self::GetFileSignature($id, $result["path"], $result["filename"], $options["crop"], ($options["maxwidth"] > 0 ? (int)$options["maxwidth"] : ""), $options["getfilesecret"]); 1300 | 1301 | return array("success" => true, "url" => $url); 1302 | } 1303 | 1304 | return array("success" => false, "error" => self::CMS_Translate("The file exists but no URL prefix option was specified that allows the file to be retrieved."), "errorcode" => "missing_url"); 1305 | } 1306 | 1307 | // Cleans up a local file cache used by DeliverFile(). Intended to be used by a cron job/scheduled task. 1308 | // The default cache time is 3 * 24 * 60 * 60 = 259200 (3 days). 1309 | public static function CleanFileCache($cachedir, $keepfor = 259200) 1310 | { 1311 | $dir = @opendir($cachedir); 1312 | if ($dir) 1313 | { 1314 | while (($file = readdir($dir)) !== false) 1315 | { 1316 | if ($file !== "." && $file !== ".." && is_dir($cachedir . "/" . $file) && file_exists($cachedir . "/" . $file . "/index.html") && filemtime($cachedir . "/" . $file . "/index.html") < time() - $keepfor) 1317 | { 1318 | $dir2 = @opendir($cachedir . "/" . $file); 1319 | if ($dir2) 1320 | { 1321 | while (($file2 = readdir($dir2)) !== false) 1322 | { 1323 | if ($file2 !== "." && $file2 !== ".." && $file2 !== "index.html") @unlink($cachedir . "/" . $file . "/" . $file2); 1324 | } 1325 | 1326 | closedir($dir2); 1327 | } 1328 | 1329 | $hasfiles = false; 1330 | $dir2 = @opendir($cachedir . "/" . $file); 1331 | if ($dir2) 1332 | { 1333 | while (($file2 = readdir($dir2)) !== false) 1334 | { 1335 | if ($file2 !== "." && $file2 !== ".." && $file2 !== "index.html") $hasfiles = true; 1336 | } 1337 | 1338 | closedir($dir2); 1339 | } 1340 | 1341 | if (!$hasfiles) 1342 | { 1343 | @unlink($cachedir . "/" . $file . "/index.html"); 1344 | @rmdir($cachedir . "/" . $file); 1345 | } 1346 | } 1347 | } 1348 | 1349 | closedir($dir); 1350 | } 1351 | } 1352 | 1353 | public function Internal_TransformStoryAssetBodyCallback($stack, &$content, $open, $tagname, &$attrs, $options) 1354 | { 1355 | // Handle 'div-embed' tags. 1356 | if ($open && $tagname === "div-embed" && $options["tag_callback_opts"]->processdivembed) 1357 | { 1358 | unset($attrs["aria-label"]); 1359 | 1360 | // Handle 'data-html' attributes. They contain the full inner HTML. 1361 | if (isset($attrs["data-html"])) 1362 | { 1363 | $data = @json_decode($attrs["data-html"], true); 1364 | unset($attrs["data-html"]); 1365 | 1366 | if (is_string($data)) 1367 | { 1368 | $data = TagFilter::Run($data, $options); 1369 | 1370 | return array("state" => $data); 1371 | } 1372 | } 1373 | } 1374 | 1375 | // Handle 'data-src-info'. 1376 | if ($open && isset($attrs["data-src-info"]) && (($pos = TagFilter::GetParentPos($stack, "div-embed")) === false || $stack[$pos]["state"] === false)) 1377 | { 1378 | $data = @json_decode($attrs["data-src-info"], true); 1379 | if (!is_array($data)) unset($attrs["data-src-info"]); 1380 | else 1381 | { 1382 | if (!$options["tag_callback_opts"]->keepsrcinfo) unset($attrs["data-src-info"]); 1383 | 1384 | $fileext = self::GetFileExtension($data["file"]["filename"]); 1385 | $image = ($tagname === "img" && self::CanResizeImage($options["tag_callback_opts"]->mimeinfomap, $fileext)); 1386 | unset($attrs["srcset"]); 1387 | 1388 | $found = false; 1389 | foreach ($options["process_attrs"] as $attr => $type) 1390 | { 1391 | if ($type === "uri" && isset($attrs[$attr])) $found = true; 1392 | } 1393 | 1394 | if (!$found) $attrs[($tagname === "a" ? "href" : "src")] = "//0.0.0.0/transform.gif"; 1395 | 1396 | foreach ($options["process_attrs"] as $attr => $type) 1397 | { 1398 | if ($type === "uri" && isset($attrs[$attr])) 1399 | { 1400 | $options2 = array( 1401 | "cachedir" => $options["tag_callback_opts"]->cachedir, 1402 | "cacheurl" => $options["tag_callback_opts"]->cacheurl, 1403 | "apidir" => $options["tag_callback_opts"]->apidir, 1404 | "apiurl" => $options["tag_callback_opts"]->apiurl, 1405 | "getfileurl" => $options["tag_callback_opts"]->getfileurl, 1406 | "getfilesecret" => $options["tag_callback_opts"]->getfilesecret, 1407 | "path" => $data["file"]["path"], 1408 | ); 1409 | 1410 | if (isset($data["download"])) $options2["download"] = $data["file"]["origfilename"]; 1411 | 1412 | if ($image) 1413 | { 1414 | $options2["maxwidth"] = (int)(isset($attrs["data-max-width"]) && $options["tag_callback_opts"]->usedatamaxwidth ? $attrs["data-max-width"] : $options["tag_callback_opts"]->maxwidth); 1415 | if (isset($data["crop"])) $options2["crop"] = $data["crop"]; 1416 | } 1417 | 1418 | $result = $this->PrecacheDeliverFile(((string)$data["id"] !== (string)$options["tag_callback_opts"]->asset["id"] ? $data["id"] : $options["tag_callback_opts"]->asset), $data["file"]["filename"], $options2); 1419 | if ($result["success"]) $attrs[$attr] = $result["url"]; 1420 | else $attrs["data-transform-error"] = $result["error"] . " (" . $result["errorcode"] . ")"; 1421 | 1422 | if (isset($options["tag_callback_opts"]->em)) $options["tag_callback_opts"]->em->Fire($options["tag_callback_opts"]->emfire . "_datasrc", array($tagname, &$attrs, $data)); 1423 | } 1424 | } 1425 | } 1426 | } 1427 | 1428 | if (!$open) 1429 | { 1430 | // Transform 'div-embed' tags with 'data-html' attributes. 1431 | if ($tagname === "/div-embed" && $options["tag_callback_opts"]->processdivembed) 1432 | { 1433 | $pos = TagFilter::GetParentPos($stack, "div-embed"); 1434 | if ($stack[$pos]["state"] !== false) $content = $stack[$pos]["state"]; 1435 | } 1436 | 1437 | // Trim content and remove empty block-level tags. Reduces storage requirements and tends to look nicer. 1438 | if (isset($options["tag_callback_opts"]->trimcontent[substr($tagname, 1)])) $content = trim(str_replace(" ", " ", $content)); 1439 | 1440 | if (isset($options["tag_callback_opts"]->removeempty[substr($tagname, 1)]) && trim($content) === "") return array("keep_tag" => false); 1441 | } 1442 | 1443 | $result = array(); 1444 | if (isset($options["tag_callback_opts"]->em)) 1445 | { 1446 | $results = $options["tag_callback_opts"]->em->Fire($options["tag_callback_opts"]->emfire, array($stack, &$content, $open, $tagname, &$attrs, $options)); 1447 | 1448 | foreach ($results as $result2) 1449 | { 1450 | $result = array_replace($result, $result2); 1451 | } 1452 | } 1453 | 1454 | return $result; 1455 | } 1456 | 1457 | // Transforms story asset body content for viewing. 1458 | public function TransformStoryAssetBody($asset, $options = array(), $lang = false) 1459 | { 1460 | if (!class_exists("TagFilter", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/tag_filter.php"; 1461 | 1462 | // Normalize options. 1463 | if (!isset($options["trimcontent"])) $options["trimcontent"] = "p,h1,h2,h3,h4,h5,h6,blockquote,pre,li"; 1464 | if (!isset($options["removeempty"])) $options["removeempty"] = "p,h1,h2,h3,h4,h5,h6,blockquote,pre,ol,ul,li,table,thead,tbody,tr"; 1465 | if (!isset($options["processdivembed"])) $options["processdivembed"] = true; 1466 | if (!isset($options["keepsrcinfo"])) $options["keepsrcinfo"] = false; 1467 | if (!isset($options["usedatamaxwidth"])) $options["usedatamaxwidth"] = true; 1468 | if (!isset($options["maxwidth"]) || !is_numeric($options["maxwidth"])) $options["maxwidth"] = -1; 1469 | if (!isset($options["mimeinfomap"]) || !is_array($options["mimeinfomap"])) $options["mimeinfomap"] = self::GetDefaultMimeInfoMap(); 1470 | if (!isset($options["cachedir"])) $options["cachedir"] = false; 1471 | if (!isset($options["cacheurl"])) $options["cacheurl"] = false; 1472 | if (!isset($options["apidir"])) $options["apidir"] = false; 1473 | if (!isset($options["apiurl"])) $options["apiurl"] = false; 1474 | if (!isset($options["getfileurl"])) $options["getfileurl"] = false; 1475 | if (!isset($options["getfilesecret"])) $options["getfilesecret"] = false; 1476 | if (!isset($options["siteurl"])) $options["siteurl"] = false; 1477 | if (!isset($options["emfire"])) $options["emfire"] = ""; 1478 | 1479 | $options["asset"] = $asset; 1480 | 1481 | $result = TagFilter::NormalizeHTMLPurifyOptions(array("allowed_tags" => $options["trimcontent"], "remove_empty" => $options["removeempty"])); 1482 | $options["trimcontent"] = $result["allowed_tags"]; 1483 | $options["removeempty"] = $result["remove_empty"]; 1484 | 1485 | $htmloptions = TagFilter::GetHTMLOptions(); 1486 | $htmloptions["tag_callback"] = array($this, "Internal_TransformStoryAssetBodyCallback"); 1487 | $htmloptions["tag_callback_opts"] = (object)$options; 1488 | 1489 | if ($lang !== false) 1490 | { 1491 | $asset["langinfo"][$lang]["body"] = TagFilter::Run($asset["langinfo"][$lang]["body"], $htmloptions); 1492 | 1493 | if ($options["siteurl"] !== false) $asset["langinfo"][$lang]["body"] = str_replace("site://", htmlspecialchars($options["siteurl"] . "/"), $asset["langinfo"][$lang]["body"]); 1494 | } 1495 | else 1496 | { 1497 | foreach ($asset["langinfo"] as $lang => $info) 1498 | { 1499 | $info["body"] = TagFilter::Run($info["body"], $htmloptions); 1500 | 1501 | if ($options["siteurl"] !== false) $info["body"] = str_replace("site://", htmlspecialchars($options["siteurl"] . "/"), $info["body"]); 1502 | 1503 | $asset["langinfo"][$lang]["body"] = $info["body"]; 1504 | } 1505 | } 1506 | 1507 | return $asset; 1508 | } 1509 | 1510 | // Generates summary information for an asset for a list view. 1511 | public static function GenerateStoryAssetSummary($asset, $options = array(), $lang = false) 1512 | { 1513 | if (!class_exists("TagFilter", false)) require_once str_replace("\\", "/", dirname(__FILE__)) . "/tag_filter.php"; 1514 | 1515 | // Normalize options. 1516 | if (!isset($options["paragraphs"]) || !is_numeric($options["paragraphs"])) $options["paragraphs"] = 2; 1517 | if (!isset($options["html"]) || !is_bool($options["html"])) $options["html"] = true; 1518 | if (!isset($options["keepbody"]) || !is_bool($options["keepbody"])) $options["keepbody"] = false; 1519 | 1520 | $htmloptions = TagFilter::GetHTMLOptions(); 1521 | 1522 | foreach ($asset["langinfo"] as $lang2 => $info) 1523 | { 1524 | if ($lang !== false && $lang !== $lang2) continue; 1525 | 1526 | $html = TagFilter::Explode($info["body"], $htmloptions); 1527 | $root = $html->Get(); 1528 | 1529 | // Extract the first image found. 1530 | $info["img"] = false; 1531 | $info["img_src"] = false; 1532 | $rows = $root->Find('img[src]'); 1533 | foreach ($rows as $row) 1534 | { 1535 | if (strncmp($row->src, "//0.0.0.0/", 10)) 1536 | { 1537 | $info["img"] = $row->GetOuterHTML(); 1538 | $info["img_src"] = $row->src; 1539 | $html->Remove($row->ID()); 1540 | 1541 | break; 1542 | } 1543 | } 1544 | 1545 | // Extract the desired number of paragraphs. 1546 | $info["summary"] = array(); 1547 | $rows = $root->Find('p'); 1548 | foreach ($rows as $row) 1549 | { 1550 | $info["summary"][] = ($options["html"] ? $row->GetOuterHTML() : $row->GetPlainText()); 1551 | 1552 | if (count($info["summary"]) >= $options["paragraphs"]) break; 1553 | } 1554 | 1555 | if (!$options["keepbody"]) unset($info["body"]); 1556 | 1557 | $asset["langinfo"][$lang2] = $info; 1558 | } 1559 | 1560 | return $asset; 1561 | } 1562 | 1563 | // Determines if the user has supplied a valid content refresh token. 1564 | public static function CanRefreshContent($validtoken, $requestkey = "refresh") 1565 | { 1566 | // If PHP sesssion are enabled and the user appears to have a valid session or request, check that first. 1567 | unset($_COOKIE["bb_valid"]); 1568 | if (session_status() !== PHP_SESSION_DISABLED) 1569 | { 1570 | if (isset($_COOKIE["bb"]) || (isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken)) 1571 | { 1572 | // Close an existing session. 1573 | $currsession = (session_status() === PHP_SESSION_ACTIVE); 1574 | if ($currsession) @session_write_close(); 1575 | 1576 | // Switch to the 'bb' session. 1577 | $prevname = @session_name("bb"); 1578 | @session_start(); 1579 | 1580 | $_SESSION["ts"] = time(); 1581 | 1582 | if (!isset($_SESSION["bb_cms_refresh_keys"])) $_SESSION["bb_cms_refresh_keys"] = array(); 1583 | if (!isset($_SESSION["bb_cms_refresh_keys"][$validtoken]) && ((isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken))) $_SESSION["bb_cms_refresh_keys"][$validtoken] = true; 1584 | $valid = isset($_SESSION["bb_cms_refresh_keys"][$validtoken]); 1585 | 1586 | @session_write_close(); 1587 | @session_name($prevname); 1588 | 1589 | // Restore previous session (if any). 1590 | if ($currsession) @session_start(); 1591 | 1592 | // Stop processing when the request is simply a heartbeat. 1593 | if (isset($_POST["bb_heartbeat"])) exit(); 1594 | 1595 | if ($valid) $_COOKIE["bb_valid"] = true; 1596 | 1597 | return $valid; 1598 | } 1599 | } 1600 | 1601 | // Fallback to using browser cookies. Only supports one active refresh token at a time. 1602 | if ((isset($_COOKIE[$requestkey]) && $_COOKIE[$requestkey] === $validtoken) || (isset($_GET[$requestkey]) && $_GET[$requestkey] === $validtoken) || (isset($_POST[$requestkey]) && $_POST[$requestkey] === $validtoken)) 1603 | { 1604 | if (!isset($_COOKIE[$requestkey]) || $_COOKIE[$requestkey] !== $validtoken) @setcookie($requestkey, $validtoken, 0, "", "", false, true); 1605 | 1606 | return true; 1607 | } 1608 | 1609 | return false; 1610 | } 1611 | 1612 | // Output a heartbeat AJAX function. Intended for keep-alive of an active refresh PHP session. 1613 | // Default request rate is every 5 minutes (5 * 60 = 300). 1614 | public static function OutputHeartbeat($every = 300) 1615 | { 1616 | if (isset($_COOKIE["bb_valid"])) 1617 | { 1618 | ?> 1619 | 1627 | 1641 | 1642 | $info) unset($asset["langinfo"][$lang]["protected"]); 1655 | } 1656 | 1657 | $key2 = md5(json_encode($key, JSON_UNESCAPED_SLASHES)); 1658 | 1659 | $basedir = $contentdir . "/" . $type; 1660 | @mkdir($basedir); 1661 | if (!file_exists($basedir . "/index.html")) @file_put_contents($basedir . "/index.html", ""); 1662 | 1663 | $basedir .= "/" . substr($key2, 0, 2); 1664 | @mkdir($basedir); 1665 | if (!file_exists($basedir . "/index.html")) @file_put_contents($basedir . "/index.html", ""); 1666 | 1667 | file_put_contents($basedir . "/assets_" . $key2 . ".dat", json_encode(array("key" => $key, "assets" => $assets), JSON_UNESCAPED_SLASHES)); 1668 | } 1669 | 1670 | // Calculates the full path and filename to cached assets. 1671 | public static function GetCachedAssetsFilename($contentdir, $type, $key) 1672 | { 1673 | $key = md5(json_encode($key, JSON_UNESCAPED_SLASHES)); 1674 | $filename = $contentdir . "/" . $type . "/" . substr($key, 0, 2) . "/assets_" . $key . ".dat"; 1675 | 1676 | return $filename; 1677 | } 1678 | 1679 | // Loads cached assets into memory. Pays heed to publish/unpublish times. 1680 | public static function LoadCachedAssets($contentdirfilename, $type = false, $key = false) 1681 | { 1682 | if ($type === false || $key === false) $filename = $contentdirfilename; 1683 | else $filename = self::GetCachedAssetsFilename($contentdirfilename, $type, $key); 1684 | 1685 | if (!file_exists($filename)) return array(); 1686 | 1687 | $data = @json_decode(file_get_contents($filename), true); 1688 | if (!is_array($data)) return array(); 1689 | $assets = $data["assets"]; 1690 | unset($data); 1691 | 1692 | $ts = time(); 1693 | $result = array(); 1694 | foreach ($assets as $num => $asset) 1695 | { 1696 | $asset = self::NormalizeAsset($asset); 1697 | unset($asset["lastupdatedby"]); 1698 | unset($asset["lastip"]); 1699 | unset($asset["protected"]); 1700 | 1701 | if ($asset["publish"] > 0 && $asset["publish"] <= $ts && ($asset["unpublish"] == 0 || $asset["unpublish"] > $ts)) $result[] = $asset; 1702 | } 1703 | 1704 | return $result; 1705 | } 1706 | 1707 | protected static function CMS_Translate() 1708 | { 1709 | $args = func_get_args(); 1710 | if (!count($args)) return ""; 1711 | 1712 | return call_user_func_array((defined("CS_TRANSLATE_FUNC") && function_exists(CS_TRANSLATE_FUNC) ? CS_TRANSLATE_FUNC : "sprintf"), $args); 1713 | } 1714 | 1715 | // Enabling debug mode can leak information (e.g. the API key). 1716 | public function SetDebug($debug) 1717 | { 1718 | $this->debug = (bool)$debug; 1719 | } 1720 | 1721 | protected function RunAPI($method, $apipath, $options = array(), $expected = 200, $encodejson = true, $decodebody = true) 1722 | { 1723 | if ($this->host === false || $this->apikey === false) return array("success" => false, "error" => self::CMS_Translate("Missing host or API key."), "errorcode" => "no_access_info"); 1724 | 1725 | $url = $this->host; 1726 | 1727 | // Handle Remoted API connections. 1728 | if ($this->fp === false && RemotedAPI::IsRemoted($this->host)) 1729 | { 1730 | $result = RemotedAPI::Connect($this->host); 1731 | if (!$result["success"]) return $result; 1732 | 1733 | $this->fp = $result["fp"]; 1734 | $url = $result["url"]; 1735 | } 1736 | 1737 | $options2 = array( 1738 | "method" => $method, 1739 | "headers" => array( 1740 | "Connection" => "keep-alive", 1741 | "X-APIKey" => $this->apikey 1742 | ) 1743 | ); 1744 | if ($this->debug) $options2["debug"] = true; 1745 | 1746 | if ($this->fp !== false) $options2["fp"] = $this->fp; 1747 | 1748 | if ($this->apisecret !== false) 1749 | { 1750 | $data = array(); 1751 | 1752 | foreach ($apipath as $key => $val) 1753 | { 1754 | if (substr($key, -2) !== "[]") $data[(string)$key] = (string)$val; 1755 | else 1756 | { 1757 | $key = substr($key, 0, -2); 1758 | if (!isset($data[$key]) || !is_array($data[$key])) $data[$key] = array(); 1759 | foreach ($val as $val2) $data[$key][] = (string)$val2; 1760 | } 1761 | } 1762 | } 1763 | 1764 | if ($encodejson && $method !== "GET") 1765 | { 1766 | $options2["headers"]["Content-Type"] = "application/json"; 1767 | $options2["body"] = json_encode($options, JSON_UNESCAPED_SLASHES); 1768 | 1769 | if ($this->apisecret !== false) $data = array_merge($data, $options); 1770 | } 1771 | else 1772 | { 1773 | $options2 = array_merge($options2, $options); 1774 | 1775 | if ($this->apisecret !== false && isset($options["postvars"])) 1776 | { 1777 | foreach ($options["postvars"] as $key => $val) 1778 | { 1779 | if (substr($key, -2) !== "[]") $data[(string)$key] = (string)$val; 1780 | else 1781 | { 1782 | $key = substr($key, 0, -2); 1783 | if (!isset($data[$key]) || !is_array($data[$key])) $data[$key] = array(); 1784 | foreach ($val as $val2) $data[$key][] = (string)$val2; 1785 | } 1786 | } 1787 | } 1788 | } 1789 | 1790 | // Generate signature. 1791 | if ($this->apisecret !== false) $options2["headers"]["X-Signature"] = base64_encode(hash_hmac("sha256", json_encode($data, JSON_UNESCAPED_SLASHES), $this->apisecret, true)); 1792 | 1793 | // Calculate the API path. 1794 | $apipath2 = ""; 1795 | foreach ($apipath as $key => $val) 1796 | { 1797 | if (!is_array($val)) $val = array($val); 1798 | foreach ($val as $val2) $apipath2 .= ($apipath2 === "" ? "?" : "&") . urlencode($key) . "=" . urlencode($val2); 1799 | } 1800 | 1801 | $result = $this->web->Process($url . $apipath2, $options2); 1802 | 1803 | if (!$result["success"] && $this->fp !== false) 1804 | { 1805 | // If the server terminated the connection, then re-establish the connection and rerun the request. 1806 | @fclose($this->fp); 1807 | $this->fp = false; 1808 | 1809 | return $this->RunAPI($method, $apipath, $options, $expected, $encodejson, $decodebody); 1810 | } 1811 | 1812 | if (!$result["success"]) return $result; 1813 | 1814 | if ($this->debug) 1815 | { 1816 | echo "------- RAW SEND START -------\n"; 1817 | echo $result["rawsend"]; 1818 | echo "------- RAW SEND END -------\n\n"; 1819 | 1820 | echo "------- RAW RECEIVE START -------\n"; 1821 | echo $result["rawrecv"]; 1822 | echo "------- RAW RECEIVE END -------\n\n"; 1823 | } 1824 | 1825 | if (isset($result["fp"]) && is_resource($result["fp"])) $this->fp = $result["fp"]; 1826 | else $this->fp = false; 1827 | 1828 | if ($result["response"]["code"] != $expected) return array("success" => false, "error" => self::CMS_Translate("Expected a %d response from the Barebones CMS API. Received '%s'.", $expected, $result["response"]["line"]), "errorcode" => "unexpected_barebones_cms_api_response", "info" => $result); 1829 | 1830 | if ($decodebody) 1831 | { 1832 | $data = json_decode($result["body"], true); 1833 | if (!is_array($data)) return array("success" => false, "error" => self::CMS_Translate("Unable to decode the server response as JSON."), "errorcode" => "expected_json", "info" => $result); 1834 | 1835 | return $data; 1836 | } 1837 | 1838 | return $result; 1839 | } 1840 | } 1841 | ?> --------------------------------------------------------------------------------