├── 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 | ?>
--------------------------------------------------------------------------------