Source of file KapostService.php
Size: 47,336 Bytes - Last Modified: 2021-12-23T10:57:38+00:00
/var/www/docs.ssmods.com/process/src/code/control/KapostService.php
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043 | <?php /** * Class KapostService * * @mixin LenovoKapostService */ class KapostService extends Controller implements PermissionProvider { /** * If set to true when the service is called the user agent of the request is checked to see if it is Kapost's XML-RPC user agent * @config KapostService.check_user_agent * @default true */ private static $check_user_agent=true; /** * Authenticator to be used for authenticating the Kapost account * @config KapostService.authenticator_class * @default MemberAuthenticator */ private static $authenticator_class='MemberAuthenticator'; /** * Authenticator to be used for authenticating the Kapost account * @config KapostService.authenticator_username_field * @default Email */ private static $authenticator_username_field='Email'; /** * Authenticator to be used for authenticating the Kapost account * @config KapostService.kapost_media_folder * @default kapost-media */ private static $kapost_media_folder='kapost-media'; /** * Tells the service what to do with duplicate media assets * Options: * smart_rename: Verifies the file is the same as the existing file and instead uses that file, otherwise it renames the file to make it unique * rename: Rename the file to make it unique * overwrite: Overwrite the duplicate resource * ignore: Ignore's the duplicate resource and returns an error to Kapost * * @config KapostService.duplicate_assets * @default smart_rename */ private static $duplicate_assets='smart_rename'; /** * Preview token expiry window in minutes * @config KapostService.preview_token_expiry * @default 10 */ private static $preview_token_expiry=10; /** * Preview data expiry window in minutes * @config KapostService.preview_data_expiry * @default 20 */ private static $preview_data_expiry=20; /** * Database Character Set * @config KapostService.database_charset * @default UTF-8 */ private static $database_charset='UTF-8'; /** * Enables filtering of the kapost thread tags in the description field * @config KapostService.filter_kapost_threads * @default true */ private static $filter_kapost_threads=false; private $exposed_methods=array( 'blogger.getUsersBlogs', 'metaWeblog.newPost', 'metaWeblog.editPost', 'metaWeblog.getPost', 'metaWeblog.getCategories', 'metaWeblog.newMediaObject', 'kapost.getPreview' ); private static $allowed_actions=array( 'preview' ); /** * Handles incoming requests to the kapost service */ public function index() { //If the request is not a post request 404 if(!$this->request->isPOST()) { if(class_exists('ErrorPage')) { $response=ErrorPage::response_for(404); if(!empty($response)) { return parent::httpError(404, $response); } } return parent::httpError(404); } //If the request is not the kapost user agent 404 if(self::config()->check_user_agent==true && $this->request->getHeader('User-Agent')!='Kapost XMLRPC::Client') { if(class_exists('ErrorPage')) { $response=ErrorPage::response_for(404); if(!empty($response)) { return parent::httpError(404, $response); } } return parent::httpError(404); } $methods=array_fill_keys($this->exposed_methods, array('function'=>array($this, 'handleRPCMethod'))); //Disable Content Negotiator and send the text/xml header (which kapost expects) ContentNegotiator::config()->enabled=false; $this->response->addHeader('Content-Type', 'text/xml'); $server=new PhpXmlRpc\Server($methods, false); $server->compress_response=true; if(Director::isDev()) { $server->setDebug(3); //Base 64 encoded debug information is included in the response } //Tell XML-RPC to re-throw the exception rather than trap it so we can allow the SilverStripe's normal error handling along side sending the xmlrpc response $server->exception_handling=2; //Force the internal encoding of the XMLRPC library to utf-8 PhpXmlRpc\PhpXmlRpc::$xmlrpc_internalencoding=self::config()->database_charset; try { return $server->service($this->request->getBody(), true); }catch(Exception $e) { //Call on SS_Log to log the error SS_Log::log($e, SS_Log::ERR); //Allow exceptions to handle the response $results=$this->extend('onException', $e); if($results && is_array($results)) { $results=array_filter($results, function($v) {return (!is_null($v) && $v instanceof PhpXmlRpc\Response);}); if(count($results)>0) { $this->generateErrorResponse($server, array_shift($results)); } } //If we're in dev mode relay the actual message to the client if(Director::isDev()) { $response=new PhpXmlRpc\Response(0, $e->getCode()+100, _t('KapostService.ERROR_MESSAGE', '_{message} in {file} line {line_number}', array( 'message'=>$e->getMessage(), 'file'=>$e->getFile(), 'line_number'=>$e->getLine() ))); }else { $response=new PhpXmlRpc\Response(0, 17, _t('KapostService.SERVER_ERROR', '_Internal server error')); } return $this->generateErrorResponse($server, $response); } } /** * Handles rendering of the preview for an object * @return string Response to send to the object */ public function preview() { $auth=$this->request->getVar('auth'); $token=KapostPreviewToken::get()->filter('Code', Convert::raw2sql($auth))->first(); //Verify the token exists and hasn't expired yet if(!empty($token) && $token!==false && $token->exists() && time()-strtotime($token->Created)<self::config()->preview_token_expiry*60 && $token->KapostRefID==$this->urlParams['ID']) { $kapostObj=KapostObject::get()->filter('KapostRefID', Convert::raw2sql($this->urlParams['ID']))->sort('"Created" DESC')->first(); if(!empty($kapostObj) && $kapostObj!==false && $kapostObj->exists()) { $previewController=$kapostObj->renderPreview(); $this->extend('updatePreviewDisplay', $kapostObj, $previewController); return $previewController; } } //Token expired or object not found if(class_exists('ErrorPage')) { $response=ErrorPage::response_for(404); if(!empty($response)) { return parent::httpError(404, $response); } } return parent::httpError(404); } /** * Handles RPC request methods * @param PhpXmlRpc\Request $request XML-RPC Request Object */ public function handleRPCMethod(PhpXmlRpc\Request $request) { $username=$request->getParam(1)->scalarval(); $password=$request->getParam(2)->scalarval(); if($this->authenticate($username, $password)) { $encoder=new PhpXmlRpc\Encoder(); $method=str_replace(array('blogger.', 'metaWeblog.', 'kapost.'), '', $request->methodname); if(!in_array($request->methodname, $this->exposed_methods) || !method_exists($this, $method)) { return $this->httpError(403, _t('KapostService.METHOD_NOT_ALLOWED', '_Action "{method}" is not allowed on class Kapost Service.', array('method'=>$request->methodname))); } //Pack params into call to method if they are not the authentication parameters $params=array(); for($i=0;$i<$request->getNumParams();$i++) { if($i!=1 && $i!=2) { $params[]=$encoder->decode($request->getParam($i)); } } //Convert the custom fields to an associtive array if(array_key_exists(1, $params) && is_array($params[1]) && array_key_exists('custom_fields', $params[1])) { $params[1]['custom_fields']=$this->struct_to_assoc($params[1]['custom_fields']); } //If transactions are supported start one for newPost and editPost if(($method=='newPost' || $method=='editPost') && DB::getConn()->supportsTransactions()) { DB::getConn()->transactionStart(); } //Call the method try { $response=call_user_func_array(array($this, $method), $params); if($response instanceof PhpXmlRpc\Response) { //If transactions are supported check the response and rollback in the case of a fault if(($method=='newPost' || $method=='editPost' || $method=='newMediaObject') && DB::getConn()->supportsTransactions()) { if($response->faultCode()!=0) { DB::getConn()->transactionRollback(); }else { DB::getConn()->transactionEnd(); } } return $response; //Response is already encoded so return } }catch(ValidationException $e) { if(DB::getConn()->supportsTransactions()) { DB::getConn()->transactionRollback(); } return new PhpXmlRpc\Response(0, 400, $e->getMessage()); } //Encode the response $response=$encoder->encode($response); if(is_object($response) && $response instanceof PhpXmlRpc\Value) { $response=new PhpXmlRpc\Response($response); if(($method=='newPost' || $method=='editPost' || $method=='newMediaObject') && DB::getConn()->supportsTransactions()) { if($response->faultCode()!=0) { DB::getConn()->transactionRollback(); }else { DB::getConn()->transactionEnd(); } } return $response; } return $this->httpError(500, _t('KapostService.INVALID_RESPONSE', '_Invalid response returned from {method}, response was: {response}', array( 'method'=>$method, 'response'=>print_r($response, true) ))); } return $this->httpError(401, _t('KapostService.AUTH_FAIL', '_Authentication Failed, please check the App Center credentials for the SilverStripe end point.')); } /** * Checks the authentication of the api request * @param string $username Username to look up * @param string $password Password to match against * @return bool Returns boolean true if authentication passes false otherwise */ protected function authenticate($username, $password) { $authenticator=$this->config()->authenticator_class; $member=$authenticator::authenticate(array( $this->config()->authenticator_username_field=>$username, 'Password'=>$password )); return (!empty($member) && $member!==false && $member->exists()==true && Permission::check('KAPOST_API_ACCESS', 'any', $member)); } /** * Converts an error to an xmlrpc response * @param int $errorCode Error code number for the error * @param string $errorMessage Error message string * @return PhpXmlRpc\Response XML-RPC response object */ public function httpError($errorCode, $errorMessage=null) { return new PhpXmlRpc\Response(0, $errorCode+10000, $errorMessage); } /** * Gets the site config or subsites for the current site * @return array Nested array of sites */ protected function getUsersBlogs($app_id) { if(SiteConfig::has_extension('SiteConfigSubsites')) { $response=array(); //Disable subsite filter Subsite::disable_subsite_filter(); $subsites=Subsite::get(); foreach($subsites as $subsite) { $response[]=array( 'blogid'=>$subsite->ID, 'blogname'=>$subsite->Title ); } //Re-enable subsite filter Subsite::disable_subsite_filter(false); return $response; } $siteConfig=SiteConfig::current_site_config(); return array( array( 'blogid'=>$siteConfig->ID, 'blogname'=>$siteConfig->Title ) ); } /** * Handles creation of a new post * @param mixed $blog_id Identifier for the current site * @param array $content Post details * @param int $publish 0 or 1 depending on whether to publish the post or not * @param bool $isPreview Is preview mode or not (defaults to false) */ protected function newPost($blog_id, $content, $publish, $isPreview=false) { //Decode all hash or array keys if(array_key_exists('custom_fields', $content)) { $hashKeys=$this->preg_grep_keys('/^_kapost_(array|hash)_/', $content['custom_fields']); if(!empty($hashKeys)) { foreach($hashKeys as $key=>$value) { //Decode the base64 encoded string $value=@base64_decode($value); if($value!==false && !empty($value)) { //Decode the array string to a php associative array $value=@json_decode($value, true); if($value!==false && !empty($value)) { //Remove the old key unset($content['custom_fields'][$key]); //Add the decoded array to the custom fields $content['custom_fields'][preg_replace('/^_kapost_(array|hash)_/', '', $key)]=$value; } } } } } //Allow extensions to handle the request $results=$this->extend('newPost', $blog_id, $content, $publish, $isPreview); if($results && is_array($results)) { $results=array_filter($results, function($v) {return !is_null($v);}); if(count($results)>0) { return array_shift($results); } } if(array_key_exists('custom_fields', $content)) { //Ensure the type is an extension of the KapostPage object if(!class_exists('Kapost'.$content['custom_fields']['kapost_custom_type']) || !class_exists('SiteTree') || !('Kapost'.$content['custom_fields']['kapost_custom_type']=='KapostPage' || is_subclass_of('Kapost'.$content['custom_fields']['kapost_custom_type'], 'KapostPage'))) { return $this->httpError(400, _t('KapostService.TYPE_NOT_KNOWN', '_The type "{type}" is not a known type', array('type'=>$content['custom_fields']['kapost_custom_type']))); } $className='Kapost'.$content['custom_fields']['kapost_custom_type']; }else { //Assume we're creating a page and set the content as such $className='KapostPage'; } $pageTitle=$content['title']; if(array_key_exists('custom_fields', $content) && array_key_exists('SS_Title', $content['custom_fields']) && !empty($content['custom_fields']['SS_Title'])) { $pageTitle=$content['custom_fields']['SS_Title']; } $menuTitle=$content['title']; if(empty($content['title']) && array_key_exists('custom_fields', $content) && array_key_exists('SS_Title', $content['custom_fields']) && !empty($content['custom_fields']['SS_Title'])) { $menuTitle=$content['custom_fields']['SS_Title']; } $obj=new $className(); $obj->Title=$pageTitle; $obj->MenuTitle=$menuTitle; $obj->Content=(self::config()->filter_kapost_threads==true ? $this->filterKapostThreads($content['description']):$content['description']); $obj->MetaDescription=(array_key_exists('custom_fields', $content) && array_key_exists('SS_MetaDescription', $content['custom_fields']) ? $content['custom_fields']['SS_MetaDescription']:null); $obj->KapostChangeType='new'; $obj->KapostAuthor=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author']:null); $obj->KapostAuthorAvatar=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author_avatar']:null); $obj->KapostConversionNotes=(array_key_exists('custom_fields', $content) && array_key_exists('SS_KapostConversionNotes', $content['custom_fields']) ? $content['custom_fields']['SS_KapostConversionNotes']:null); $obj->KapostRefID=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_post_id']:null); $obj->ToPublish=$publish; $obj->IsKapostPreview=$isPreview; $obj->write(); //Fallback for tests where the kapost_post_id is missing if(!array_key_exists('custom_fields', $content)) { $obj->KapostRefID=$className.'_'.$obj->ID; $obj->write(); } //Allow extensions to adjust the new page $this->extend('updateNewKapostPage', $obj, $blog_id, $content, $publish, $isPreview); //Validate the incoming content before writing $valid=$obj->validate_incoming(); if(!$valid->valid()) { return new PhpXmlRpc\Response(0, 400, $valid->message()); } return $obj->KapostRefID; } /** * Handles editing of a given post * @param mixed $content_id Identifier for the post * @param array $content Post details * @param int $publish 0 or 1 depending on whether to publish the post or not * @param bool $isPreview Is preview mode or not (defaults to false) */ protected function editPost($content_id, $content, $publish, $isPreview=false) { //Decode all hash or array keys if(array_key_exists('custom_fields', $content)) { $hashKeys=$this->preg_grep_keys('/^_kapost_(array|hash)_/', $content['custom_fields']); if(!empty($hashKeys)) { foreach($hashKeys as $key=>$value) { //Decode the base64 encoded string $value=@base64_decode($value); if($value!==false && !empty($value)) { //Decode the array string to a php associative array $value=@json_decode($value, true); if($value!==false && !empty($value)) { //Remove the old key unset($content['custom_fields'][$key]); //Add the decoded array to the custom fields $content['custom_fields'][preg_replace('/^_kapost_(array|hash)_/', '', $key)]=$value; } } } } } //Allow extensions to handle the request $results=$this->extend('editPost', $content_id, $content, $publish, $isPreview); if($results && is_array($results)) { $results=array_filter($results, function($v) {return !is_null($v);}); if(count($results)>0) { return array_shift($results); } } //Ensure the type is an extension of the KapostPage object if((array_key_exists('custom_fields', $content) && (!class_exists('Kapost'.$content['custom_fields']['kapost_custom_type']) || !class_exists('SiteTree') || !('Kapost'.$content['custom_fields']['kapost_custom_type']=='KapostPage' || is_subclass_of('Kapost'.$content['custom_fields']['kapost_custom_type'], 'KapostPage')))) || (!array_key_exists('custom_fields', $content) && !class_exists('SiteTree'))) { return $this->httpError(400, _t('KapostService.TYPE_NOT_KNOWN', '_The type "{type}" is not a known type', array('type'=>$content['custom_fields']['kapost_custom_type']))); } //Assume we're looking for a page //Clear Versioned Versioned to stage $oldReadingStage=Versioned::current_stage(); Versioned::reset(); $page=SiteTree::get()->filter('KapostRefID', Convert::raw2sql($content_id))->first(); //Switch Versioned back Versioned::reading_stage($oldReadingStage); $pageTitle=$content['title']; if(array_key_exists('custom_fields', $content) && array_key_exists('SS_Title', $content['custom_fields']) && !empty($content['custom_fields']['SS_Title'])) { $pageTitle=$content['custom_fields']['SS_Title']; } $menuTitle=$content['title']; if(empty($content['title']) && array_key_exists('custom_fields', $content) && array_key_exists('SS_Title', $content['custom_fields']) && !empty($content['custom_fields']['SS_Title'])) { $menuTitle=$content['custom_fields']['SS_Title']; } $kapostObj=KapostObject::get()->filter('KapostRefID', Convert::raw2sql($content_id))->first(); if(!empty($kapostObj) && $kapostObj!==false && $kapostObj->exists()) { $kapostObj->Title=$pageTitle; $kapostObj->MenuTitle=$menuTitle; $kapostObj->Content=(self::config()->filter_kapost_threads==true ? $this->filterKapostThreads($content['description']):$content['description']); $kapostObj->MetaDescription=(array_key_exists('custom_fields', $content) && array_key_exists('SS_MetaDescription', $content['custom_fields']) ? $content['custom_fields']['SS_MetaDescription']:null); $kapostObj->LinkedPageID=(!empty($page) && $page!==false && $page->exists() ? $page->ID:$kapostObj->LinkedPageID); $kapostObj->KapostRefID=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_post_id']:null); $kapostObj->KapostAuthor=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author']:null); $kapostObj->KapostAuthorAvatar=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author_avatar']:null); $kapostObj->KapostConversionNotes=(array_key_exists('custom_fields', $content) && array_key_exists('SS_KapostConversionNotes', $content['custom_fields']) ? $content['custom_fields']['SS_KapostConversionNotes']:null); $kapostObj->ToPublish=$publish; $kapostObj->IsKapostPreview=$isPreview; $kapostObj->write(); //Allow extensions to adjust the existing object $this->extend('updateEditKapostPage', $kapostObj, $content_id, $content, $publish, $isPreview); //Validate the incoming content $valid=$kapostObj->validate_incoming(); if(!$valid->valid()) { return new PhpXmlRpc\Response(0, 400, $valid->message()); } return true; }else { $className=(array_key_exists('custom_fields', $content) ? 'Kapost'.$content['custom_fields']['kapost_custom_type']:'KapostPage'); $obj=new $className(); $obj->Title=$pageTitle; $obj->MenuTitle=$menuTitle; $obj->Content=(self::config()->filter_kapost_threads==true ? $this->filterKapostThreads($content['description']):$content['description']); $obj->MetaDescription=(array_key_exists('custom_fields', $content) && array_key_exists('SS_MetaDescription', $content['custom_fields']) ? $content['custom_fields']['SS_MetaDescription']:null); $obj->KapostChangeType='edit'; $obj->LinkedPageID=(!empty($page) && $page!==false && $page->exists() ? $page->ID:0); $obj->KapostRefID=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_post_id']:null); $obj->KapostAuthor=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author']:null); $obj->KapostAuthorAvatar=(array_key_exists('custom_fields', $content) ? $content['custom_fields']['kapost_author_avatar']:null); $obj->KapostConversionNotes=(array_key_exists('custom_fields', $content) && array_key_exists('SS_KapostConversionNotes', $content['custom_fields']) ? $content['custom_fields']['SS_KapostConversionNotes']:null); $obj->ToPublish=$publish; $obj->IsKapostPreview=$isPreview; $obj->write(); //Allow extensions to adjust the new page $this->extend('updateEditKapostPage', $obj, $content_id, $content, $publish, $isPreview); //Validate the incoming content $valid=$obj->validate_incoming(); if(!$valid->valid()) { return new PhpXmlRpc\Response(0, 400, $valid->message()); } return true; } //Can't find the object so return a 404 code return new PhpXmlRpc\Response(0, 404, _t('KapostService.INVALID_POST_ID', '_Invalid post ID.')); } /** * Gets the details of a post from the system * @param mixed $content_id ID of the post in the system */ protected function getPost($content_id) { $results=$this->extend('getPost', $content_id); if($results && is_array($results)) { $results=array_filter($results, function($v) {return !is_null($v);}); if(count($results)>0) { return array_shift($results); } } //If we don't have a SiteTree class and extensions didn't handle the request return an invalid post if(!class_exists('SiteTree')) { return new PhpXmlRpc\Response(0, 404, _t('KapostService.INVALID_POST_ID', '_Invalid post ID.')); } //Reset Versioned Versioned::reset(); $page=SiteTree::get()->filter('KapostRefID', Convert::raw2sql($content_id))->first(); if(!empty($page) && $page!==false && $page->exists()) { $postMeta=array( 'title'=>$page->Title, 'description'=>$page->Content, 'mt_keywords'=>'', 'mt_excerpt'=>'', 'categories'=>array('ss_page'), 'permaLink'=>$page->AbsoluteLink(), 'custom_fields'=>array( array('id'=>'SS_Title', 'key'=>'SS_Title', 'value'=>$page->Title), array('id'=>'SS_MetaDescription', 'key'=>'SS_MetaDescription', 'value'=>$page->MetaDescription) ) ); //Allow extensions to modify the page meta $results=$this->extend('updatePageMeta', $page); if(count($results)>0) { for($i=0;$i<count($results);$i++) { $postMeta=$this->mergeResultArray($postMeta, $results[$i]); } } return $postMeta; }else { $kapostObj=KapostObject::get()->filter('KapostRefID', Convert::raw2sql($content_id))->first(); if(!empty($kapostObj) && $kapostObj!==false && $kapostObj->exists()) { $postMeta=array( 'title'=>$kapostObj->Title, 'description'=>$kapostObj->Content, 'mt_keywords'=>'', 'mt_excerpt'=>'', 'categories'=>array('ss_page'), 'permaLink'=>Controller::join_links(Director::absoluteBaseURL(), 'admin/kapost/KapostObject/EditForm/field/KapostObject/item', $kapostObj->ID, 'edit'), 'custom_fields'=>array( array('id'=>'SS_Title', 'key'=>'SS_Title', 'value'=>$kapostObj->Title), array('id'=>'SS_MetaDescription', 'key'=>'SS_MetaDescription', 'value'=>$kapostObj->MetaDescription), array('id'=>'SS_KapostConversionNotes', 'key'=>'SS_KapostConversionNotes', 'value'=>$kapostObj->KapostConversionNotes) ) ); //Allow extensions to modify the page meta $results=$this->extend('updateObjectMeta', $kapostObj); if(count($results)>0) { for($i=0;$i<count($results);$i++) { $postMeta=$this->mergeResultArray($postMeta, $results[$i]); } } return $postMeta; } } return new PhpXmlRpc\Response(0, 404, _t('KapostService.INVALID_POST_ID', '_Invalid post ID.')); } /** * Gets the categories * @param mixed $blog_id ID of the blog * @return array Array of categories */ protected function getCategories($blog_id) { $categories=array(); //If we have a SiteTree class add the classes if(class_exists('SiteTree')) { $pageClasses=ClassInfo::subclassesFor('SiteTree'); foreach($pageClasses as $class) { if($class!='SiteTree') { $categories[]=array( 'categoryId'=>'ss_'.strtolower($class), 'categoryName'=>singleton($class)->i18n_singular_name(), 'parentId'=>0 ); } } } $results=$this->extend('getCategories', $blog_id); if($results && is_array($results)) { $results=array_filter($results, function($v) {return !is_null($v);}); if(count($results)>0) { for($i=0;$i<count($results);$i++) { $categories=array_merge($categories, $results[$i]); } } } return $categories; } /** * Handles media objects from kapost * @param mixed $blog_id Site Config related to this content object * @param array $content Content object to be handled * @return PhpXmlRpc\Response XML-RPC Response object */ protected function newMediaObject($blog_id, $content) { $fileName=$content['name']; $validator=new Upload_Validator(array('name'=>$fileName)); $validator->setAllowedExtensions(File::config()->allowed_extensions); //Verify we have a valid extension if($validator->isValidExtension()==false) { return $this->httpError(403, _t('KapostService.FILE_NOT_ALLOWED', '_File extension is not allowed')); } //Generate default filename $nameFilter=FileNameFilter::create(); $file=$nameFilter->filter($fileName); while($file[0]=='_' || $file[0]=='.') { $file=substr($file, 1); } $doubleBarrelledExts=array('.gz', '.bz', '.bz2'); $ext=""; if(preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) { $file=$matches[1]; $ext=$matches[2]; // Special case for double-barrelled if(in_array($ext, $doubleBarrelledExts) && preg_match('/^(.*)(\.[^.]+)$/', $file, $matches)) { $file=$matches[1]; $ext=$matches[2].$ext; } } $origFile=$file; //Find the kapost media folder $kapostMediaFolder=Folder::find_or_make($this->config()->kapost_media_folder); if(file_exists($kapostMediaFolder->getFullPath().'/'.$file.$ext)) { if(self::config()->duplicate_assets=='overwrite') { $obj=File::get()->filter('Filename', Convert::raw2sql($kapostMediaFolder->Filename.$file.$ext))->first(); if(!empty($obj) && $obj!==false && $obj->ID>0) { //Update the Title for the image $obj->Title=(!empty($content['alt']) ? $content['alt']:str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $obj->Name))); $obj->write(); //Write the file to the file system $f=fopen($kapostMediaFolder->getFullPath().'/'.$file.$ext, 'w'); fwrite($f, $content['bits']); fclose($f); return array( 'id'=>$obj->ID, 'url'=>$obj->getAbsoluteURL() ); } return $this->httpError(404, _t('KapostService.FILE_NOT_FOUND', '_File not found')); }else if(self::config()->duplicate_assets=='ignore') { return $this->httpError(409, _t('KapostService.DUPLICATE_FILE', '_Duplicate file detected, please rename the file and try again')); }else { if(self::config()->duplicate_assets=='smart_rename' && file_exists($kapostMediaFolder->getFullPath().'/'.$file.$ext)) { $obj=File::get()->filter('Filename', Convert::raw2sql($kapostMediaFolder->Filename.$file.$ext))->first(); if(!empty($obj) && $obj!==false && $obj->ID>0) { $fileHash=sha1_file($kapostMediaFolder->getFullPath().'/'.$file.$ext); if($fileHash==sha1($content['bits'])) { return array( 'id'=>$obj->ID, 'url'=>$obj->getAbsoluteURL() ); } } } $i = 1; while(file_exists($kapostMediaFolder->getFullPath().'/'.$file.$ext)) { $i++; $oldFile=$file; if(strpos($file, '.')!==false) { $file = preg_replace('/[0-9]*(\.[^.]+$)/', $i.'\\1', $file); }else if(strpos($file, '_')!==false) { $file=preg_replace('/_([^_]+$)/', '_'.$i, $file); }else { $file.='_'.$i; } if($oldFile==$file && $i > 2) { return $this->httpError(500, _t('KapostService.FILE_RENAME_FAIL', '_Could not fix {filename} with {attempts} attempts', array('filename'=>$file.$ext, 'attempts'=>$i))); } } //Write the file to the file system $f=fopen($kapostMediaFolder->getFullPath().'/'.$file.$ext, 'w'); fwrite($f, $content['bits']); fclose($f); //Write the file to the database $className=File::get_class_for_file_extension(substr($ext, 1)); $obj=new $className(); $obj->Name=$file.$ext; $obj->Title=(!empty($content['alt']) ? $content['alt']:str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $obj->Name))); $obj->FileName=$kapostMediaFolder->getRelativePath().'/'.$file.$ext; $obj->ParentID=$kapostMediaFolder->ID; //If subsites is enabled add it to the correct subsite if(File::has_extension('FileSubsites')) { $obj->SubsiteID=$blog_id; } $obj->write(); $this->extend('updateNewMediaAsset', $blog_id, $content, $obj); return array( 'id'=>$obj->ID, 'url'=>$obj->getAbsoluteURL() ); } }else { //Write the file to the file system $f=fopen($kapostMediaFolder->getFullPath().'/'.$file.$ext, 'w'); fwrite($f, $content['bits']); fclose($f); //Write the file to the database $className=File::get_class_for_file_extension(substr($ext, 1)); $obj=new $className(); $obj->Name=$file.$ext; $obj->Title=(!empty($content['alt']) ? $content['alt']:str_replace(array('-','_'), ' ', preg_replace('/\.[^.]+$/', '', $obj->Name))); $obj->FileName=$kapostMediaFolder->getRelativePath().'/'.$file.$ext; $obj->ParentID=$kapostMediaFolder->ID; //If subsites is enabled add it to the correct subsite if(File::has_extension('FileSubsites')) { $obj->SubsiteID=$blog_id; } $obj->write(); $this->extend('updateNewMediaAsset', $blog_id, $content, $obj); return array( 'id'=>$obj->ID, 'url'=>$obj->getAbsoluteURL() ); } } /** * Handles rendering of the preview * @param mixed $blog_id Identifier for the current site * @param array $content Post details * @param mixed $content_id Identifier for the post */ protected function getPreview($blog_id, $content, $content_id) { $results=$this->extend('getPreview', $blog_id, $content, $content_id); if($results && is_array($results)) { $results=array_filter($results, function($v) {return !is_null($v);}); if(count($results)>0) { return array_shift($results); } } //Detect if the record already exists or not so we can decide whether to create a new record or edit an existing $existing=KapostObject::get()->filter('KapostRefID', Convert::raw2sql($content_id))->first(); if(!empty($existing) && $existing!==false && $existing->exists()) { $resultID=$content_id; $this->editPost($content_id, $content, false, true); }else { $resultID=$this->newPost($blog_id, $content, false, true); //Make sure we got the kapost hash back or an id if we got an object back we assume that it's a response if(is_object($resultID)) { return $resultID; } //Find the object $existing=KapostObject::get()->filter('KapostRefID', Convert::raw2sql($resultID))->first(); } //Make sure we got the kapost hash back or an id if we got an object back we assume that it's a response if(is_object($resultID)) { return $resultID; } //Generate a preview token record $token=new KapostPreviewToken(); $token->Code=sha1(uniqid(time().$resultID)); $token->KapostRefID=$resultID; $token->write(); //Return the details to kapost return array( 'url'=>Controller::join_links(Director::absoluteBaseURL(), 'kapost-service/preview', $resultID, '?auth='.$token->Code), 'id'=>$resultID ); } /** * Converts a struct to an associtive array based on the key value pair in the struct * @param array $struct Input struct to be converted * @return array Associtive array matching the struct */ final protected function struct_to_assoc($struct) { $result=array(); foreach($struct as $item) { if(array_key_exists('key', $item) && array_key_exists('value', $item)) { if(array_key_exists($item['key'], $result)) { user_error('Duplicate key detected in struct entry, content overwritten by the last entry: [New: '.print_r($item, true).'] [Previous: '.print_r($result[$item['key']], true).']', E_USER_WARNING); } $result[$item['key']]=$item['value']; }else { user_error('Key/Value pair not detected in struct entry: '.print_r($item, true), E_USER_NOTICE); } } return $result; } /** * Merges two arrays, overwriting the keys in the left array with the right array recurrsivly. Meaning that if a value in the right array is it self an array and the key exists in the left array it recurses into it. * @param array $leftArray Left array to merge into * @param array $rightArray Right array to merge from * @return array Resulting array */ private function mergeResultArray($leftArray, $rightArray) { foreach($rightArray as $key=>$value) { if(is_array($value) && array_key_exists($key, $leftArray)) { $leftArray[$key]=array_merge($leftArray[$key], $value); }else { $leftArray[$key]=$value; } } return $leftArray; } /** * Gets all of the values who's keys match a given expression * @param string $pattern Regular expression patter to run against the keys * @param array $input Input array * @param int $flags Flags to pass to preg_grep * @return array Array of key value pairs who's keys matched the expression * * @see preg_grep() */ private function preg_grep_keys($pattern, $input, $flags=0) { return array_intersect_key($input, array_flip(preg_grep($pattern, array_keys($input), $flags))); } /** * Filters the kapost content to remove the thread tags from a Kapost WYSIWYG * @param string $html HTML to filter the tags from * @return string HTML with tags filtered */ public function filterKapostThreads($html) { return preg_replace('/<span(\s+)thread="(.*?)"(\s+)class="thread">(.*?)<\/span>/', '$4', $html); } /** * Finds a file record based on the url of the file, this is needed because Kapost doesn't seem to send anything back other than the url in the cms * @param string $url Absolute url to the file * @return File Returns the file instance representing the url, or boolean false if it's not found */ public static function find_file_by_url($url) { $url=Director::makeRelative($url); if($url) { $file=File::get()->filter('Filename', Convert::raw2sql($url))->first(); if(!empty($file) && $file!==false && $file->ID>0) { return $file; } } return false; } /** * Takes the xmlrpc object and generates the response to be set back * @param PhpXmlRpc\Server $server XML-RPC Server instance * @param PhpXmlRpc\Response $r XML-RPC Response object to relay to client * @return string Response to be sent to the client */ protected function generateErrorResponse(PhpXmlRpc\Server $server, PhpXmlRpc\Response $r) { $this->response->addHeader('Content-Type', $r->content_type); $this->response->addHeader('Vary', 'Accept-Charset'); $payload='<?xml version="1.0"?>'; if(empty($r->payload)) { $r->serialize(); } $payload=$payload.$r->payload; return $payload; } /** * Return a map of permission codes to add to the dropdown shown in the Security section of the CMS. * @return array Map of permission codes */ public function providePermissions() { return array( 'KAPOST_API_ACCESS'=>array( 'category'=>_t('KapostService.KAPOST_BRIDGE', '_Kapost Bridge'), 'name'=>_t('KapostService.PERMISSION_API_ACCESS', '_Kapost API Access'), 'help'=>_t('KapostService.PERMISSION_API_ACCESS_DESC', '_Access the XML-RPC Endpoint for Kapost to communicate with') ), ); } } ?> |